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.for-closed-social
@ -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``` |
@ -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 | |||
} |
@ -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 | |||
} |
@ -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 | |||
} |
@ -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)) | |||
} | |||
} |