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:
|
test:
|
||||||
GO111MODULE=on $(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' $(PACKAGES)
|
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
|
.PHONY: coverage
|
||||||
coverage:
|
coverage:
|
||||||
@hash gocovmerge > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
@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
|
; List of reasons why a Pull Request or Issue can be locked
|
||||||
LOCK_REASONS=Too heated,Off-topic,Resolved,Spam
|
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]
|
[cors]
|
||||||
; More information about CORS can be found here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#The_HTTP_response_headers
|
; 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)
|
; 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
|
- `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`)
|
## CORS (`cors`)
|
||||||
|
|
||||||
- `ENABLED`: **false**: enable cors headers (disabled by default)
|
- `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)
|
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{
|
Verification: &api.PayloadCommitVerification{
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "unsigned",
|
Reason: "gpg.error.not_signed_commit",
|
||||||
Signature: "",
|
Signature: "",
|
||||||
Payload: "",
|
Payload: "",
|
||||||
},
|
},
|
||||||
|
|
|
@ -94,7 +94,7 @@ func getExpectedFileResponseForUpdate(commitID, treePath string) *api.FileRespon
|
||||||
},
|
},
|
||||||
Verification: &api.PayloadCommitVerification{
|
Verification: &api.PayloadCommitVerification{
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "unsigned",
|
Reason: "gpg.error.not_signed_commit",
|
||||||
Signature: "",
|
Signature: "",
|
||||||
Payload: "",
|
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_COPY_PATH = tmp/local-repo-mssql
|
||||||
LOCAL_WIKI_PATH = tmp/local-wiki-mssql
|
LOCAL_WIKI_PATH = tmp/local-wiki-mssql
|
||||||
|
|
||||||
|
[repository.signing]
|
||||||
|
SIGNING_KEY = none
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
SSH_DOMAIN = localhost
|
SSH_DOMAIN = localhost
|
||||||
HTTP_PORT = 3003
|
HTTP_PORT = 3003
|
||||||
|
|
|
@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mysql/gitea-repositories
|
||||||
LOCAL_COPY_PATH = tmp/local-repo-mysql
|
LOCAL_COPY_PATH = tmp/local-repo-mysql
|
||||||
LOCAL_WIKI_PATH = tmp/local-wiki-mysql
|
LOCAL_WIKI_PATH = tmp/local-wiki-mysql
|
||||||
|
|
||||||
|
[repository.signing]
|
||||||
|
SIGNING_KEY = none
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
SSH_DOMAIN = localhost
|
SSH_DOMAIN = localhost
|
||||||
HTTP_PORT = 3001
|
HTTP_PORT = 3001
|
||||||
|
|
|
@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mysql8/gitea-repositories
|
||||||
LOCAL_COPY_PATH = tmp/local-repo-mysql8
|
LOCAL_COPY_PATH = tmp/local-repo-mysql8
|
||||||
LOCAL_WIKI_PATH = tmp/local-wiki-mysql8
|
LOCAL_WIKI_PATH = tmp/local-wiki-mysql8
|
||||||
|
|
||||||
|
[repository.signing]
|
||||||
|
SIGNING_KEY = none
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
SSH_DOMAIN = localhost
|
SSH_DOMAIN = localhost
|
||||||
HTTP_PORT = 3004
|
HTTP_PORT = 3004
|
||||||
|
|
|
@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-pgsql/gitea-repositories
|
||||||
LOCAL_COPY_PATH = tmp/local-repo-pgsql
|
LOCAL_COPY_PATH = tmp/local-repo-pgsql
|
||||||
LOCAL_WIKI_PATH = tmp/local-wiki-pgsql
|
LOCAL_WIKI_PATH = tmp/local-wiki-pgsql
|
||||||
|
|
||||||
|
[repository.signing]
|
||||||
|
SIGNING_KEY = none
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
SSH_DOMAIN = localhost
|
SSH_DOMAIN = localhost
|
||||||
HTTP_PORT = 3002
|
HTTP_PORT = 3002
|
||||||
|
|
|
@ -53,7 +53,7 @@ func getExpectedDeleteFileResponse(u *url.URL) *api.FileResponse {
|
||||||
},
|
},
|
||||||
Verification: &api.PayloadCommitVerification{
|
Verification: &api.PayloadCommitVerification{
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "",
|
Reason: "gpg.error.not_signed_commit",
|
||||||
Signature: "",
|
Signature: "",
|
||||||
Payload: "",
|
Payload: "",
|
||||||
},
|
},
|
||||||
|
|
|
@ -108,7 +108,7 @@ func getExpectedFileResponseForRepofilesCreate(commitID string) *api.FileRespons
|
||||||
},
|
},
|
||||||
Verification: &api.PayloadCommitVerification{
|
Verification: &api.PayloadCommitVerification{
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "unsigned",
|
Reason: "gpg.error.not_signed_commit",
|
||||||
Signature: "",
|
Signature: "",
|
||||||
Payload: "",
|
Payload: "",
|
||||||
},
|
},
|
||||||
|
@ -175,7 +175,7 @@ func getExpectedFileResponseForRepofilesUpdate(commitID, filename string) *api.F
|
||||||
},
|
},
|
||||||
Verification: &api.PayloadCommitVerification{
|
Verification: &api.PayloadCommitVerification{
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "unsigned",
|
Reason: "gpg.error.not_signed_commit",
|
||||||
Signature: "",
|
Signature: "",
|
||||||
Payload: "",
|
Payload: "",
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,6 +17,9 @@ ROOT = integrations/gitea-integration-sqlite/gitea-repositories
|
||||||
LOCAL_COPY_PATH = tmp/local-repo-sqlite
|
LOCAL_COPY_PATH = tmp/local-repo-sqlite
|
||||||
LOCAL_WIKI_PATH = tmp/local-wiki-sqlite
|
LOCAL_WIKI_PATH = tmp/local-wiki-sqlite
|
||||||
|
|
||||||
|
[repository.signing]
|
||||||
|
SIGNING_KEY = none
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
SSH_DOMAIN = localhost
|
SSH_DOMAIN = localhost
|
||||||
HTTP_PORT = 3003
|
HTTP_PORT = 3003
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
|
@ -80,6 +81,12 @@ func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
|
||||||
return key, nil
|
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.
|
// GetGPGImportByKeyID returns the import public armored key by given KeyID.
|
||||||
func GetGPGImportByKeyID(keyID string) (*GPGKeyImport, error) {
|
func GetGPGImportByKeyID(keyID string) (*GPGKeyImport, error) {
|
||||||
key := new(GPGKeyImport)
|
key := new(GPGKeyImport)
|
||||||
|
@ -356,8 +363,11 @@ func DeleteGPGKey(doer *User, id int64) (err error) {
|
||||||
// CommitVerification represents a commit validation of signature
|
// CommitVerification represents a commit validation of signature
|
||||||
type CommitVerification struct {
|
type CommitVerification struct {
|
||||||
Verified bool
|
Verified bool
|
||||||
|
Warning bool
|
||||||
Reason string
|
Reason string
|
||||||
SigningUser *User
|
SigningUser *User
|
||||||
|
CommittingUser *User
|
||||||
|
SigningEmail string
|
||||||
SigningKey *GPGKey
|
SigningKey *GPGKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,6 +377,17 @@ type SignCommit struct {
|
||||||
*UserCommit
|
*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) {
|
func readerFromBase64(s string) (io.Reader, error) {
|
||||||
bs, err := base64.StdEncoding.DecodeString(s)
|
bs, err := base64.StdEncoding.DecodeString(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -424,37 +445,194 @@ func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
|
||||||
return pkey.VerifySignature(h, s)
|
return pkey.VerifySignature(h, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseCommitWithSignature check if signature is good against keystore.
|
func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification {
|
||||||
func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
|
//Generating hash of commit
|
||||||
if c.Signature != nil && c.Committer != nil {
|
hash, err := populateHash(sig.Hash, []byte(payload))
|
||||||
//Parsing signature
|
if err != nil { //Skipping failed to generate hash
|
||||||
sig, err := extractSignature(c.Signature.Signature)
|
log.Error("PopulateHash: %v", err)
|
||||||
if err != nil { //Skipping failed to extract sign
|
|
||||||
log.Error("SignatureRead err: %v", err)
|
|
||||||
return &CommitVerification{
|
return &CommitVerification{
|
||||||
|
CommittingUser: committer,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "gpg.error.extract_sign",
|
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
|
//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
|
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
|
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
|
// We can expect this to often be an ErrUserNotExist. in the case
|
||||||
// it is not, however, it is important to log it.
|
// it is not, however, it is important to log it.
|
||||||
if !IsErrUserNotExist(err) {
|
if !IsErrUserNotExist(err) {
|
||||||
log.Error("GetUserByEmail: %v", err)
|
log.Error("GetUserByEmail: %v", err)
|
||||||
}
|
|
||||||
return &CommitVerification{
|
return &CommitVerification{
|
||||||
|
CommittingUser: committer,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "gpg.error.no_committer_account",
|
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)
|
keys, err := ListGPGKeys(committer.ID)
|
||||||
if err != nil { //Skipping failed to get gpg keys of user
|
if err != nil { //Skipping failed to get gpg keys of user
|
||||||
log.Error("ListGPGKeys: %v", err)
|
log.Error("ListGPGKeys: %v", err)
|
||||||
return &CommitVerification{
|
return &CommitVerification{
|
||||||
|
CommittingUser: committer,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||||
}
|
}
|
||||||
|
@ -463,10 +641,11 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
//Pre-check (& optimization) that emails attached to key can be attached to the commiter email and can validate
|
//Pre-check (& optimization) that emails attached to key can be attached to the commiter email and can validate
|
||||||
canValidate := false
|
canValidate := false
|
||||||
lowerCommiterEmail := strings.ToLower(c.Committer.Email)
|
email := ""
|
||||||
for _, e := range k.Emails {
|
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
|
canValidate = true
|
||||||
|
email = e.Email
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -474,56 +653,102 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
|
||||||
continue //Skip this key
|
continue //Skip this key
|
||||||
}
|
}
|
||||||
|
|
||||||
//Generating hash of commit
|
commitVerification := hashAndVerifyWithSubKeys(sig, c.Signature.Payload, k, committer, committer, email)
|
||||||
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
|
if commitVerification != nil {
|
||||||
if err != nil { //Skipping ailed to generate hash
|
return commitVerification
|
||||||
log.Error("PopulateHash: %v", err)
|
|
||||||
return &CommitVerification{
|
|
||||||
Verified: false,
|
|
||||||
Reason: "gpg.error.generate_hash",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//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
|
if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
|
||||||
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
|
// OK we should try the default key
|
||||||
if err != nil { //Skipping ailed to generate hash
|
gpgSettings := git.GPGSettings{
|
||||||
log.Error("PopulateHash: %v", err)
|
Sign: true,
|
||||||
return &CommitVerification{
|
KeyID: setting.Repository.Signing.SigningKey,
|
||||||
Verified: false,
|
Name: setting.Repository.Signing.SigningName,
|
||||||
Reason: "gpg.error.generate_hash",
|
Email: setting.Repository.Signing.SigningEmail,
|
||||||
}
|
}
|
||||||
}
|
if err := gpgSettings.LoadPublicKeyContent(); err != nil {
|
||||||
if err := verifySign(sig, hash, sk); err == nil {
|
log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
|
||||||
return &CommitVerification{ //Everything is ok
|
} else if commitVerification := verifyWithGPGSettings(&gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
|
||||||
Verified: true,
|
if commitVerification.Reason == BadSignature {
|
||||||
Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, sk.KeyID),
|
defaultReason = BadSignature
|
||||||
SigningUser: committer,
|
} else {
|
||||||
SigningKey: sk,
|
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
|
return &CommitVerification{ //Default at this stage
|
||||||
|
CommittingUser: committer,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "gpg.error.no_gpg_keys_found",
|
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{
|
return &CommitVerification{
|
||||||
Verified: false, //Default value
|
CommittingUser: committer,
|
||||||
Reason: "gpg.error.not_signed_commit", //Default value
|
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.
|
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
|
||||||
|
|
|
@ -38,6 +38,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
|
"github.com/mcuadros/go-version"
|
||||||
"github.com/unknwon/com"
|
"github.com/unknwon/com"
|
||||||
ini "gopkg.in/ini.v1"
|
ini "gopkg.in/ini.v1"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
@ -1126,7 +1127,20 @@ func CleanUpMigrateInfo(repo *Repository) (*Repository, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// initRepoCommit temporarily changes with work directory.
|
// 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
|
var stderr string
|
||||||
if _, stderr, err = process.GetManager().ExecDir(-1,
|
if _, stderr, err = process.GetManager().ExecDir(-1,
|
||||||
tmpPath, fmt.Sprintf("initRepoCommit (git add): %s", tmpPath),
|
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)
|
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),
|
tmpPath, fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath),
|
||||||
git.GitExecutable, "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
|
env,
|
||||||
"-m", "Initial commit"); err != nil {
|
git.GitExecutable, args...); err != nil {
|
||||||
return fmt.Errorf("git commit: %s", stderr)
|
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 {
|
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.
|
// 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),
|
fmt.Sprintf("initRepository(git clone): %s", repoPath),
|
||||||
|
env,
|
||||||
git.GitExecutable, "clone", repoPath, tmpDir,
|
git.GitExecutable, "clone", repoPath, tmpDir,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1282,7 +1330,7 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, opts C
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply changes and commit.
|
// 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)
|
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{
|
commitTreeOpts := git.CommitTreeOpts{
|
||||||
Message: message,
|
Message: message,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sign, signingKey := repo.SignWikiCommit(doer)
|
||||||
|
if sign {
|
||||||
|
commitTreeOpts.KeyID = signingKey
|
||||||
|
} else {
|
||||||
|
commitTreeOpts.NoGPGSign = true
|
||||||
|
}
|
||||||
if hasMasterBranch {
|
if hasMasterBranch {
|
||||||
commitTreeOpts.Parents = []string{"HEAD"}
|
commitTreeOpts.Parents = []string{"HEAD"}
|
||||||
}
|
}
|
||||||
|
@ -307,11 +314,19 @@ func (repo *Repository) DeleteWikiPage(doer *User, wikiName string) (err error)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
message := "Delete page '" + wikiName + "'"
|
message := "Delete page '" + wikiName + "'"
|
||||||
|
commitTreeOpts := git.CommitTreeOpts{
|
||||||
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, git.CommitTreeOpts{
|
|
||||||
Message: message,
|
Message: message,
|
||||||
Parents: []string{"HEAD"},
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -498,3 +498,11 @@ func GetFullCommitID(repoPath, shortID string) (string, error) {
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(commitID), nil
|
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
|
gogitRepo *gogit.Repository
|
||||||
gogitStorage *filesystem.Storage
|
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`
|
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
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ type CommitTreeOpts struct {
|
||||||
Message string
|
Message string
|
||||||
KeyID string
|
KeyID string
|
||||||
NoGPGSign bool
|
NoGPGSign bool
|
||||||
|
AlwaysSign bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitTree creates a commit from a given tree id for the user with provided message
|
// CommitTree creates a commit from a given tree id for the user with provided message
|
||||||
|
@ -90,7 +91,7 @@ func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOp
|
||||||
_, _ = messageBytes.WriteString(opts.Message)
|
_, _ = messageBytes.WriteString(opts.Message)
|
||||||
_, _ = messageBytes.WriteString("\n")
|
_, _ = 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))
|
cmd.AddArguments(fmt.Sprintf("-S%s", opts.KeyID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ package git
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
@ -86,3 +87,30 @@ func RefEndName(refStr string) string {
|
||||||
|
|
||||||
return refStr
|
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{
|
Verification: &api.PayloadCommitVerification{
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "",
|
Reason: "gpg.error.not_signed_commit",
|
||||||
Signature: "",
|
Signature: "",
|
||||||
Payload: "",
|
Payload: "",
|
||||||
},
|
},
|
||||||
|
|
|
@ -261,7 +261,6 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t
|
||||||
return "", fmt.Errorf("Unable to get git version: %v", err)
|
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
|
// Because this may call hooks we should pass in the environment
|
||||||
env := append(os.Environ(),
|
env := append(os.Environ(),
|
||||||
"GIT_AUTHOR_NAME="+authorSig.Name,
|
"GIT_AUTHOR_NAME="+authorSig.Name,
|
||||||
|
@ -271,14 +270,22 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t
|
||||||
"GIT_COMMITTER_EMAIL="+committerSig.Email,
|
"GIT_COMMITTER_EMAIL="+committerSig.Email,
|
||||||
"GIT_COMMITTER_DATE="+commitTimeStr,
|
"GIT_COMMITTER_DATE="+commitTimeStr,
|
||||||
)
|
)
|
||||||
|
|
||||||
messageBytes := new(bytes.Buffer)
|
messageBytes := new(bytes.Buffer)
|
||||||
_, _ = messageBytes.WriteString(message)
|
_, _ = messageBytes.WriteString(message)
|
||||||
_, _ = messageBytes.WriteString("\n")
|
_, _ = messageBytes.WriteString("\n")
|
||||||
|
|
||||||
args := []string{"commit-tree", treeHash, "-p", "HEAD"}
|
args := []string{"commit-tree", treeHash, "-p", "HEAD"}
|
||||||
if version.Compare(binVersion, "2.0.0", ">=") {
|
|
||||||
|
// 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")
|
args = append(args, "--no-gpg-sign")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
commitHash, stderr, err := process.GetManager().ExecDirEnvStdIn(5*time.Minute,
|
commitHash, stderr, err := process.GetManager().ExecDirEnvStdIn(5*time.Minute,
|
||||||
t.basePath,
|
t.basePath,
|
||||||
|
|
|
@ -18,10 +18,16 @@ func GetPayloadCommitVerification(commit *git.Commit) *structs.PayloadCommitVeri
|
||||||
verification.Signature = commit.Signature.Signature
|
verification.Signature = commit.Signature.Signature
|
||||||
verification.Payload = commit.Signature.Payload
|
verification.Payload = commit.Signature.Payload
|
||||||
}
|
}
|
||||||
if verification.Reason != "" {
|
if commitVerification.SigningUser != nil {
|
||||||
|
verification.Signer = &structs.PayloadUser{
|
||||||
|
Name: commitVerification.SigningUser.Name,
|
||||||
|
Email: commitVerification.SigningUser.Email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verification.Verified = commitVerification.Verified
|
||||||
verification.Reason = commitVerification.Reason
|
verification.Reason = commitVerification.Reason
|
||||||
} else if verification.Verified {
|
if verification.Reason == "" && !verification.Verified {
|
||||||
verification.Reason = "unsigned"
|
verification.Reason = "gpg.error.not_signed_commit"
|
||||||
}
|
}
|
||||||
return verification
|
return verification
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,16 @@ var (
|
||||||
Issue struct {
|
Issue struct {
|
||||||
LockReasons []string
|
LockReasons []string
|
||||||
} `ini:"repository.issue"`
|
} `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: "",
|
AnsiCharset: "",
|
||||||
ForcePrivate: false,
|
ForcePrivate: false,
|
||||||
|
@ -122,6 +132,25 @@ var (
|
||||||
}{
|
}{
|
||||||
LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","),
|
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
|
RepoRootPath string
|
||||||
ScriptType = "bash"
|
ScriptType = "bash"
|
||||||
|
|
|
@ -94,6 +94,7 @@ type PayloadCommitVerification struct {
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
Signature string `json:"signature"`
|
Signature string `json:"signature"`
|
||||||
|
Signer *PayloadUser `json:"signer"`
|
||||||
Payload string `json:"payload"`
|
Payload string `json:"payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1974,12 +1974,15 @@ mark_as_unread = Mark as unread
|
||||||
mark_all_as_read = Mark all as read
|
mark_all_as_read = Mark all as read
|
||||||
|
|
||||||
[gpg]
|
[gpg]
|
||||||
|
default_key=Signed with default key
|
||||||
error.extract_sign = Failed to extract signature
|
error.extract_sign = Failed to extract signature
|
||||||
error.generate_hash = Failed to generate hash of commit
|
error.generate_hash = Failed to generate hash of commit
|
||||||
error.no_committer_account = No account linked to committer's email address
|
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.no_gpg_keys_found = "No known key found for this signature in database"
|
||||||
error.not_signed_commit = "Not a signed commit"
|
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.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]
|
[units]
|
||||||
error.no_unit_allowed_repo = You are not allowed to access any section of this repository.
|
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{display:inline-block;vertical-align:top}
|
||||||
.inline-grouped-list>.ui{display:block;margin-top:5px;margin-bottom:10px}
|
.inline-grouped-list>.ui{display:block;margin-top:5px;margin-bottom:10px}
|
||||||
.inline-grouped-list>.ui:first-child{margin-top:1px}
|
.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{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-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}
|
.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.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,.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 .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,.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 .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}
|
.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 {
|
.lines-num {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
text-align: right !important;
|
text-align: right !important;
|
||||||
|
|
|
@ -1212,6 +1212,15 @@
|
||||||
border-bottom-left-radius: 0;
|
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 {
|
&.isSigned.isVerified {
|
||||||
border: 1px solid #21ba45;
|
border: 1px solid #21ba45;
|
||||||
background: fade(#21ba45, 10%);
|
background: fade(#21ba45, 10%);
|
||||||
|
|
|
@ -507,6 +507,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Get("/swagger", misc.Swagger)
|
m.Get("/swagger", misc.Swagger)
|
||||||
}
|
}
|
||||||
m.Get("/version", misc.Version)
|
m.Get("/version", misc.Version)
|
||||||
|
m.Get("/signing-key.gpg", misc.SigningKey)
|
||||||
m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
|
m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
|
||||||
m.Post("/markdown/raw", misc.MarkdownRaw)
|
m.Post("/markdown/raw", misc.MarkdownRaw)
|
||||||
|
|
||||||
|
@ -771,6 +772,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile)
|
m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile)
|
||||||
}, reqRepoWriter(models.UnitTypeCode), reqToken())
|
}, reqRepoWriter(models.UnitTypeCode), reqToken())
|
||||||
}, reqRepoReader(models.UnitTypeCode))
|
}, reqRepoReader(models.UnitTypeCode))
|
||||||
|
m.Get("/signing-key.gpg", misc.SigningKey)
|
||||||
m.Group("/topics", func() {
|
m.Group("/topics", func() {
|
||||||
m.Combo("").Get(repo.ListTopics).
|
m.Combo("").Get(repo.ListTopics).
|
||||||
Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
|
Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"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
|
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
|
||||||
func ToVerification(c *git.Commit) *api.PayloadCommitVerification {
|
func ToVerification(c *git.Commit) *api.PayloadCommitVerification {
|
||||||
verif := models.ParseCommitWithSignature(c)
|
verif := models.ParseCommitWithSignature(c)
|
||||||
var signature, payload string
|
commitVerification := &api.PayloadCommitVerification{
|
||||||
if c.Signature != nil {
|
|
||||||
signature = c.Signature.Signature
|
|
||||||
payload = c.Signature.Payload
|
|
||||||
}
|
|
||||||
return &api.PayloadCommitVerification{
|
|
||||||
Verified: verif.Verified,
|
Verified: verif.Verified,
|
||||||
Reason: verif.Reason,
|
Reason: verif.Reason,
|
||||||
Signature: signature,
|
|
||||||
Payload: payload,
|
|
||||||
}
|
}
|
||||||
|
if c.Signature != nil {
|
||||||
|
commitVerification.Signature = c.Signature.Signature
|
||||||
|
commitVerification.Payload = c.Signature.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
|
// 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"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/cache"
|
"code.gitea.io/gitea/modules/cache"
|
||||||
|
@ -28,6 +29,11 @@ import (
|
||||||
// Merge merges pull request to base repository.
|
// Merge merges pull request to base repository.
|
||||||
// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
|
// 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) {
|
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 {
|
if err = pr.GetHeadRepo(); err != nil {
|
||||||
return fmt.Errorf("GetHeadRepo: %v", err)
|
return fmt.Errorf("GetHeadRepo: %v", err)
|
||||||
} else if err = pr.GetBaseRepo(); err != nil {
|
} 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())
|
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.
|
// Merge commits.
|
||||||
switch mergeStyle {
|
switch mergeStyle {
|
||||||
case models.MergeStyleMerge:
|
case models.MergeStyleMerge:
|
||||||
|
@ -183,10 +213,15 @@ 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())
|
return fmt.Errorf("git merge --no-ff --no-commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
sig := doer.NewGitSig()
|
if signArg == "" {
|
||||||
if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil {
|
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())
|
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:
|
case models.MergeStyleRebase:
|
||||||
// Checkout head branch
|
// Checkout head branch
|
||||||
if err := git.NewCommand("checkout", "-b", stagingBranch, trackingBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil {
|
if err := git.NewCommand("checkout", "-b", stagingBranch, trackingBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil {
|
||||||
|
@ -223,10 +258,15 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set custom message and author and create merge commit
|
// Set custom message and author and create merge commit
|
||||||
sig := doer.NewGitSig()
|
if signArg == "" {
|
||||||
if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil {
|
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())
|
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:
|
case models.MergeStyleSquash:
|
||||||
// Merge with squash
|
// Merge with squash
|
||||||
|
@ -234,9 +274,15 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
|
||||||
return fmt.Errorf("git merge --squash [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String())
|
return fmt.Errorf("git merge --squash [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String())
|
||||||
}
|
}
|
||||||
sig := pr.Issue.Poster.NewGitSig()
|
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 {
|
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())
|
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:
|
default:
|
||||||
return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
|
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
|
headUser = doer
|
||||||
}
|
}
|
||||||
|
|
||||||
env := models.FullPushingEnvironment(
|
env = models.FullPushingEnvironment(
|
||||||
headUser,
|
headUser,
|
||||||
doer,
|
doer,
|
||||||
pr.BaseRepo,
|
pr.BaseRepo,
|
||||||
|
|
|
@ -26,6 +26,16 @@
|
||||||
<img class="ui avatar image" src="{{AvatarLink .Commit.Author.Email}}" />
|
<img class="ui avatar image" src="{{AvatarLink .Commit.Author.Email}}" />
|
||||||
<strong>{{.Commit.Author.Name}}</strong>
|
<strong>{{.Commit.Author.Name}}</strong>
|
||||||
{{end}}
|
{{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>
|
<span class="text grey" id="authored-time">{{TimeSince .Commit.Author.When $.Lang}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="seven wide right aligned column">
|
<div class="seven wide right aligned column">
|
||||||
|
@ -50,15 +60,36 @@
|
||||||
{{if .Commit.Signature}}
|
{{if .Commit.Signature}}
|
||||||
{{if .Verification.Verified }}
|
{{if .Verification.Verified }}
|
||||||
<div class="ui bottom attached positive message">
|
<div class="ui bottom attached positive message">
|
||||||
|
{{if ne .Verification.SigningUser.ID 0}}
|
||||||
<i class="green lock icon"></i>
|
<i class="green lock icon"></i>
|
||||||
<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
|
<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
|
||||||
<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> <{{.Commit.Committer.Email}}>
|
<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>
|
<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>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="ui bottom attached message">
|
<div class="ui bottom attached message">
|
||||||
<i class="grey unlock icon"></i>
|
<i class="grey unlock icon"></i>
|
||||||
{{.i18n.Tr .Verification.Reason}}
|
{{.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>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -56,12 +56,21 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="sha">
|
<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}}
|
{{ShortSha .ID.String}}
|
||||||
{{if .Signature}}
|
{{if .Signature}}
|
||||||
<div class="ui detail icon button">
|
<div class="ui detail icon button">
|
||||||
{{if .Verification.Verified}}
|
{{if .Verification.Verified}}
|
||||||
|
{{if ne .Verification.SigningUser.ID 0}}
|
||||||
<i title="{{.Verification.Reason}}" class="lock green icon"></i>
|
<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}}
|
{{else}}
|
||||||
<i title="{{$.i18n.Tr .Verification.Reason}}" class="unlock icon"></i>
|
<i title="{{$.i18n.Tr .Verification.Reason}}" class="unlock icon"></i>
|
||||||
{{end}}
|
{{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": {
|
"/repos/{owner}/{repo}/stargazers": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"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}": {
|
"/teams/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -9525,6 +9581,9 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Signature"
|
"x-go-name": "Signature"
|
||||||
},
|
},
|
||||||
|
"signer": {
|
||||||
|
"$ref": "#/definitions/PayloadUser"
|
||||||
|
},
|
||||||
"verified": {
|
"verified": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "Verified"
|
"x-go-name": "Verified"
|
||||||
|
|
Reference in a new issue