diff options
author | zeripath <art27@cantab.net> | 2019-10-16 14:42:42 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-16 14:42:42 +0100 |
commit | fcb535c5c3b6b782d9242028fed4cd8c027c4e41 (patch) | |
tree | 49c49fd1f040b9dcd600ec8e381df80532bc2701 /models/repo_sign.go | |
parent | 1b72690cb82302b24f41d2beaa5df5592709f4d3 (diff) | |
download | gitea-fcb535c5c3b6b782d9242028fed4cd8c027c4e41.tar.gz gitea-fcb535c5c3b6b782d9242028fed4cd8c027c4e41.zip |
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.
Diffstat (limited to 'models/repo_sign.go')
-rw-r--r-- | models/repo_sign.go | 303 |
1 files changed, 303 insertions, 0 deletions
diff --git a/models/repo_sign.go b/models/repo_sign.go new file mode 100644 index 0000000000..bac69f76a8 --- /dev/null +++ b/models/repo_sign.go @@ -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 +} |