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