From 77c9fca2d05f8e6e93d15e457ff37b86ae852bc4 Mon Sep 17 00:00:00 2001 From: Savely Krasovsky Date: Tue, 23 Aug 2022 14:41:55 +0000 Subject: feat(gitaly-git2go): sign commits with OpenPGP key --- Makefile | 2 +- NOTICE | 90 +++++++++++++++++++++ cmd/gitaly-git2go/apply.go | 27 ++++--- cmd/gitaly-git2go/cherry_pick.go | 15 ++-- cmd/gitaly-git2go/commit.go | 6 +- cmd/gitaly-git2go/commit/commit.go | 29 ++++--- cmd/gitaly-git2go/conflicts.go | 2 +- cmd/gitaly-git2go/git2goutil/commit.go | 47 +++++++++++ cmd/gitaly-git2go/git2goutil/sign.go | 43 ++++++++++ cmd/gitaly-git2go/git2goutil/test_sign.go | 49 +++++++++++ cmd/gitaly-git2go/main.go | 2 +- cmd/gitaly-git2go/merge.go | 25 +++--- cmd/gitaly-git2go/rebase.go | 1 + cmd/gitaly-git2go/resolve_conflicts.go | 19 +++-- cmd/gitaly-git2go/revert.go | 15 ++-- cmd/gitaly-git2go/submodule.go | 18 +++-- go.mod | 2 + go.sum | 5 ++ internal/backup/storage_service_sink.go | 6 +- internal/git/command_factory.go | 2 +- internal/git2go/apply.go | 7 +- internal/git2go/apply_test.go | 14 ++-- internal/git2go/cherry_pick.go | 10 ++- internal/git2go/commit.go | 36 ++------- internal/git2go/commit_test.go | 69 ++++++++++++++-- internal/git2go/executor.go | 2 + internal/git2go/merge.go | 3 + internal/git2go/rebase.go | 4 + internal/git2go/resolve_conflicts.go | 2 + internal/git2go/revert.go | 4 + internal/git2go/submodule.go | 7 +- internal/git2go/testdata/publicKey.gpg | Bin 0 -> 260 bytes internal/git2go/testdata/signingKey.gpg | Bin 0 -> 297 bytes internal/gitaly/config/config.go | 3 +- .../gitaly/service/operations/apply_patch_test.go | 6 +- internal/gitaly/service/operations/commit_files.go | 2 +- .../service/remote/update_remote_mirror_test.go | 2 +- internal/testhelper/testhelper.go | 2 + 38 files changed, 460 insertions(+), 118 deletions(-) create mode 100644 cmd/gitaly-git2go/git2goutil/commit.go create mode 100644 cmd/gitaly-git2go/git2goutil/sign.go create mode 100644 cmd/gitaly-git2go/git2goutil/test_sign.go create mode 100644 internal/git2go/testdata/publicKey.gpg create mode 100644 internal/git2go/testdata/signingKey.gpg diff --git a/Makefile b/Makefile index c61517e61..157721c1b 100644 --- a/Makefile +++ b/Makefile @@ -264,7 +264,7 @@ find_go_sources = $(shell find ${SOURCE_DIR} -type d \( -name ruby # TEST_PACKAGES: packages which shall be tested run_go_tests = PATH='${SOURCE_DIR}/internal/testhelper/testdata/home/bin:${PATH}' \ TEST_TMP_DIR='${TEST_TMP_DIR}' \ - ${GOTESTSUM} --format ${TEST_FORMAT} --junitfile ${TEST_REPORT} --jsonfile ${TEST_FULL_OUTPUT} -- -ldflags '${GO_LDFLAGS}' -tags '${SERVER_BUILD_TAGS},${GIT2GO_BUILD_TAGS}' ${TEST_OPTIONS} ${TEST_PACKAGES} + ${GOTESTSUM} --format ${TEST_FORMAT} --junitfile ${TEST_REPORT} --jsonfile ${TEST_FULL_OUTPUT} -- -ldflags '${GO_LDFLAGS}' -tags '${SERVER_BUILD_TAGS},${GIT2GO_BUILD_TAGS},gitaly_test_signing' ${TEST_OPTIONS} ${TEST_PACKAGES} ## Test options passed to `dlv test`. DEBUG_OPTIONS ?= $(patsubst -%,-test.%,${TEST_OPTIONS}) diff --git a/NOTICE b/NOTICE index 3ab61f6ad..867970232 100644 --- a/NOTICE +++ b/NOTICE @@ -2710,6 +2710,36 @@ LICENSE - github.com/Azure/go-autorest/tracing See the License for the specific language governing permissions and limitations under the License. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LICENSE - github.com/ProtonMail/go-crypto +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LICENSE.txt - github.com/aws/aws-sdk-go @@ -6754,6 +6784,66 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LICENSE - github.com/cloudflare/circl +Copyright (c) 2019 Cloudflare. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Cloudflare nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LICENSE - github.com/cloudflare/tableflip Copyright (c) 2017-2018, Cloudflare. All rights reserved. diff --git a/cmd/gitaly-git2go/apply.go b/cmd/gitaly-git2go/apply.go index 3a7f65b79..e5cc1339a 100644 --- a/cmd/gitaly-git2go/apply.go +++ b/cmd/gitaly-git2go/apply.go @@ -42,12 +42,14 @@ func (iter *patchIterator) Value() git2go.Patch { return iter.value } func (iter *patchIterator) Err() error { return iter.error } type applySubcommand struct { - gitBinaryPath string + gitBinaryPath string + signingKeyPath string } func (cmd *applySubcommand) Flags() *flag.FlagSet { fs := flag.NewFlagSet("apply", flag.ExitOnError) fs.StringVar(&cmd.gitBinaryPath, "git-binary-path", "", "Path to the Git binary.") + fs.StringVar(&cmd.signingKeyPath, "signing-key", "", "Path to the OpenPGP signing key.") return fs } @@ -126,30 +128,35 @@ func (cmd *applySubcommand) applyPatch( } } - patchedTree, err := patchedIndex.WriteTreeTo(repo) + patchedTreeOID, err := patchedIndex.WriteTreeTo(repo) if err != nil { return nil, fmt.Errorf("write patched tree: %w", err) } + patchedTree, err := repo.LookupTree(patchedTreeOID) + if err != nil { + return nil, fmt.Errorf("lookup tree: %w", err) + } author := git.Signature(patch.Author) - patchedCommitOID, err := repo.CreateCommitFromIds("", &author, committer, patch.Message, patchedTree, parentCommitOID) + patchedCommitID, err := git2goutil.NewCommitSubmitter(repo, cmd.signingKeyPath). + Commit(&author, committer, git.MessageEncodingUTF8, patch.Message, patchedTree, parentCommit) if err != nil { return nil, fmt.Errorf("create commit: %w", err) } - return patchedCommitOID, nil + return patchedCommitID, nil } // threeWayMerge attempts a three-way merge as a fallback if applying the patch fails. // Fallback three-way merge is only possible if the patch records the pre-image blobs // and the repository contains them. It works as follows: // -// 1. An index that contains only the pre-image blobs of the patch is built. This is done -// by calling `git apply --build-fake-ancestor`. The tree of the index is the fake -// ancestor tree. -// 2. The fake ancestor tree is patched to produce the post-image tree of the patch. -// 3. Three-way merge is performed with fake ancestor tree as the common ancestor, the -// base commit's tree as our tree and the patched fake ancestor tree as their tree. +// 1. An index that contains only the pre-image blobs of the patch is built. This is done +// by calling `git apply --build-fake-ancestor`. The tree of the index is the fake +// ancestor tree. +// 2. The fake ancestor tree is patched to produce the post-image tree of the patch. +// 3. Three-way merge is performed with fake ancestor tree as the common ancestor, the +// base commit's tree as our tree and the patched fake ancestor tree as their tree. func (cmd *applySubcommand) threeWayMerge( ctx context.Context, repo *git.Repository, diff --git a/cmd/gitaly-git2go/cherry_pick.go b/cmd/gitaly-git2go/cherry_pick.go index ebf29759f..1a185a7e2 100644 --- a/cmd/gitaly-git2go/cherry_pick.go +++ b/cmd/gitaly-git2go/cherry_pick.go @@ -102,21 +102,26 @@ func (cmd *cherryPickSubcommand) cherryPick(ctx context.Context, r *git2go.Cherr } } - tree, err := index.WriteTreeTo(repo) + treeOID, err := index.WriteTreeTo(repo) if err != nil { return "", fmt.Errorf("could not write tree: %w", err) } + tree, err := repo.LookupTree(treeOID) + if err != nil { + return "", fmt.Errorf("lookup tree: %w", err) + } - if tree.Equal(ours.TreeId()) { + if treeOID.Equal(ours.TreeId()) { return "", git2go.EmptyError{} } committer := git.Signature(git2go.NewSignature(r.CommitterName, r.CommitterMail, r.CommitterDate)) - commit, err := repo.CreateCommitFromIds("", pick.Author(), &committer, r.Message, tree, ours.Id()) + commitID, err := git2goutil.NewCommitSubmitter(repo, r.SigningKey). + Commit(pick.Author(), &committer, git.MessageEncodingUTF8, r.Message, tree, ours) if err != nil { - return "", fmt.Errorf("could not create cherry-pick commit: %w", err) + return "", fmt.Errorf("create not create cherry-pick commit: %w", err) } - return commit.String(), nil + return commitID.String(), nil } diff --git a/cmd/gitaly-git2go/commit.go b/cmd/gitaly-git2go/commit.go index cd0a1a31d..0dbf90d24 100644 --- a/cmd/gitaly-git2go/commit.go +++ b/cmd/gitaly-git2go/commit.go @@ -12,8 +12,10 @@ import ( type commitSubcommand struct{} -func (commitSubcommand) Flags() *flag.FlagSet { return flag.NewFlagSet("commit", flag.ExitOnError) } +func (cmd *commitSubcommand) Flags() *flag.FlagSet { + return flag.NewFlagSet("commit", flag.ExitOnError) +} -func (commitSubcommand) Run(ctx context.Context, decoder *gob.Decoder, encoder *gob.Encoder) error { +func (cmd *commitSubcommand) Run(ctx context.Context, decoder *gob.Decoder, encoder *gob.Encoder) error { return commit.Run(ctx, decoder, encoder) } diff --git a/cmd/gitaly-git2go/commit/commit.go b/cmd/gitaly-git2go/commit/commit.go index 57ca1f586..e26acd559 100644 --- a/cmd/gitaly-git2go/commit/commit.go +++ b/cmd/gitaly-git2go/commit/commit.go @@ -15,7 +15,7 @@ import ( // Run runs the commit subcommand. func Run(ctx context.Context, decoder *gob.Decoder, encoder *gob.Encoder) error { - var params git2go.CommitParams + var params git2go.CommitCommand if err := decoder.Decode(¶ms); err != nil { return err } @@ -27,8 +27,8 @@ func Run(ctx context.Context, decoder *gob.Decoder, encoder *gob.Encoder) error }) } -func commit(ctx context.Context, params git2go.CommitParams) (string, error) { - repo, err := git2goutil.OpenRepository(params.Repository) +func commit(ctx context.Context, request git2go.CommitCommand) (string, error) { + repo, err := git2goutil.OpenRepository(request.Repository) if err != nil { return "", fmt.Errorf("open repository: %w", err) } @@ -38,20 +38,20 @@ func commit(ctx context.Context, params git2go.CommitParams) (string, error) { return "", fmt.Errorf("new index: %w", err) } - var parents []*git.Oid - if params.Parent != "" { - parentOID, err := git.NewOid(params.Parent) + var parents []*git.Commit + if request.Parent != "" { + parentOID, err := git.NewOid(request.Parent) if err != nil { return "", fmt.Errorf("parse base commit oid: %w", err) } - parents = []*git.Oid{parentOID} - baseCommit, err := repo.LookupCommit(parentOID) if err != nil { return "", fmt.Errorf("lookup commit: %w", err) } + parents = []*git.Commit{baseCommit} + baseTree, err := baseCommit.Tree() if err != nil { return "", fmt.Errorf("lookup tree: %w", err) @@ -62,7 +62,7 @@ func commit(ctx context.Context, params git2go.CommitParams) (string, error) { } } - for _, action := range params.Actions { + for _, action := range request.Actions { if err := apply(action, repo, index); err != nil { if git.IsErrorClass(err, git.ErrorClassIndex) { err = git2go.IndexError(err.Error()) @@ -76,10 +76,15 @@ func commit(ctx context.Context, params git2go.CommitParams) (string, error) { if err != nil { return "", fmt.Errorf("write tree: %w", err) } + tree, err := repo.LookupTree(treeOID) + if err != nil { + return "", fmt.Errorf("lookup tree: %w", err) + } - author := git.Signature(params.Author) - committer := git.Signature(params.Committer) - commitID, err := repo.CreateCommitFromIds("", &author, &committer, params.Message, treeOID, parents...) + author := git.Signature(request.Author) + committer := git.Signature(request.Committer) + commitID, err := git2goutil.NewCommitSubmitter(repo, request.SigningKey). + Commit(&author, &committer, git.MessageEncodingUTF8, request.Message, tree, parents...) if err != nil { if git.IsErrorClass(err, git.ErrorClassInvalid) { return "", git2go.InvalidArgumentError(err.Error()) diff --git a/cmd/gitaly-git2go/conflicts.go b/cmd/gitaly-git2go/conflicts.go index 1e2fbb4e0..954483cad 100644 --- a/cmd/gitaly-git2go/conflicts.go +++ b/cmd/gitaly-git2go/conflicts.go @@ -32,7 +32,7 @@ func (cmd *conflictsSubcommand) Run(_ context.Context, decoder *gob.Decoder, enc return encoder.Encode(res) } -func (conflictsSubcommand) conflicts(request git2go.ConflictsCommand) git2go.ConflictsResult { +func (*conflictsSubcommand) conflicts(request git2go.ConflictsCommand) git2go.ConflictsResult { repo, err := git2goutil.OpenRepository(request.Repository) if err != nil { return conflictError(codes.Internal, fmt.Errorf("could not open repository: %w", err).Error()) diff --git a/cmd/gitaly-git2go/git2goutil/commit.go b/cmd/gitaly-git2go/git2goutil/commit.go new file mode 100644 index 000000000..7f45640ad --- /dev/null +++ b/cmd/gitaly-git2go/git2goutil/commit.go @@ -0,0 +1,47 @@ +package git2goutil + +import ( + "fmt" + + git "github.com/libgit2/git2go/v33" +) + +// CommitSubmitter is the helper struct to make signed Commits conveniently. +type CommitSubmitter struct { + Repo *git.Repository + SigningKeyPath string +} + +// NewCommitSubmitter creates a new CommitSubmitter. +func NewCommitSubmitter(repo *git.Repository, signingKeyPath string) *CommitSubmitter { + return &CommitSubmitter{ + Repo: repo, + SigningKeyPath: signingKeyPath, + } +} + +// Commit commits a commit with or without OpenPGP signature depends on SigningKeyPath value. +func (cs *CommitSubmitter) Commit( + author, committer *git.Signature, + messageEncoding git.MessageEncoding, + message string, + tree *git.Tree, + parents ...*git.Commit, +) (*git.Oid, error) { + commitBytes, err := cs.Repo.CreateCommitBuffer(author, committer, messageEncoding, message, tree, parents...) + if err != nil { + return nil, err + } + + signature, err := CreateCommitSignature(cs.SigningKeyPath, string(commitBytes)) + if err != nil { + return nil, fmt.Errorf("create commit signature: %w", err) + } + + commitID, err := cs.Repo.CreateCommitWithSignature(string(commitBytes), signature, "") + if err != nil { + return nil, err + } + + return commitID, nil +} diff --git a/cmd/gitaly-git2go/git2goutil/sign.go b/cmd/gitaly-git2go/git2goutil/sign.go new file mode 100644 index 000000000..b8437d9ad --- /dev/null +++ b/cmd/gitaly-git2go/git2goutil/sign.go @@ -0,0 +1,43 @@ +//go:build !gitaly_test_signing + +package git2goutil + +import ( + "bytes" + "fmt" + "os" + "strings" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// CreateCommitSignature reads the given signing key and produces PKCS#7 detached signature. +// When the path to the signing key is not present, an empty signature is returned. +func CreateCommitSignature(signingKeyPath, contentToSign string) (string, error) { + if signingKeyPath == "" { + return "", nil + } + + file, err := os.Open(signingKeyPath) + if err != nil { + return "", fmt.Errorf("open file: %w", err) + } + + entity, err := openpgp.ReadEntity(packet.NewReader(file)) + if err != nil { + return "", fmt.Errorf("read entity: %w", err) + } + + sigBuf := new(bytes.Buffer) + if err := openpgp.ArmoredDetachSignText( + sigBuf, + entity, + strings.NewReader(contentToSign), + &packet.Config{}, + ); err != nil { + return "", fmt.Errorf("sign commit: %w", err) + } + + return sigBuf.String(), nil +} diff --git a/cmd/gitaly-git2go/git2goutil/test_sign.go b/cmd/gitaly-git2go/git2goutil/test_sign.go new file mode 100644 index 000000000..990344910 --- /dev/null +++ b/cmd/gitaly-git2go/git2goutil/test_sign.go @@ -0,0 +1,49 @@ +//go:build gitaly_test_signing + +package git2goutil + +import ( + "bytes" + "fmt" + "os" + "strings" + "time" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// CreateCommitSignature reads the given signing key and produces PKCS#7 detached signature. +// When the path to the signing key is not present, an empty signature is returned. +// Test version creates deterministic signature which is the same with the same data every run. +func CreateCommitSignature(signingKeyPath, contentToSign string) (string, error) { + if signingKeyPath == "" { + return "", nil + } + + file, err := os.Open(signingKeyPath) + if err != nil { + return "", fmt.Errorf("open file: %w", err) + } + + entity, err := openpgp.ReadEntity(packet.NewReader(file)) + if err != nil { + return "", fmt.Errorf("read entity: %w", err) + } + + sigBuf := new(bytes.Buffer) + if err := openpgp.ArmoredDetachSignText( + sigBuf, + entity, + strings.NewReader(contentToSign), + &packet.Config{ + Time: func() time.Time { + return time.Date(2022, 8, 20, 11, 22, 33, 0, time.UTC) + }, + }, + ); err != nil { + return "", fmt.Errorf("sign commit: %w", err) + } + + return sigBuf.String(), nil +} diff --git a/cmd/gitaly-git2go/main.go b/cmd/gitaly-git2go/main.go index e8a2c2c7b..8ee0e7fd0 100644 --- a/cmd/gitaly-git2go/main.go +++ b/cmd/gitaly-git2go/main.go @@ -27,7 +27,7 @@ type subcmd interface { var subcommands = map[string]subcmd{ "apply": &applySubcommand{}, "cherry-pick": &cherryPickSubcommand{}, - "commit": commitSubcommand{}, + "commit": &commitSubcommand{}, "conflicts": &conflictsSubcommand{}, "merge": &mergeSubcommand{}, "rebase": &rebaseSubcommand{}, diff --git a/cmd/gitaly-git2go/merge.go b/cmd/gitaly-git2go/merge.go index 4ab3ef9fd..21922b24a 100644 --- a/cmd/gitaly-git2go/merge.go +++ b/cmd/gitaly-git2go/merge.go @@ -18,8 +18,7 @@ import ( type mergeSubcommand struct{} func (cmd *mergeSubcommand) Flags() *flag.FlagSet { - flags := flag.NewFlagSet("merge", flag.ExitOnError) - return flags + return flag.NewFlagSet("merge", flag.ExitOnError) } func (cmd *mergeSubcommand) Run(_ context.Context, decoder *gob.Decoder, encoder *gob.Encoder) error { @@ -32,7 +31,7 @@ func (cmd *mergeSubcommand) Run(_ context.Context, decoder *gob.Decoder, encoder request.AuthorDate = time.Now() } - commitID, err := merge(request) + commitID, err := cmd.merge(request) return encoder.Encode(git2go.Result{ CommitID: commitID, @@ -40,7 +39,7 @@ func (cmd *mergeSubcommand) Run(_ context.Context, decoder *gob.Decoder, encoder }) } -func merge(request git2go.MergeCommand) (string, error) { +func (cmd *mergeSubcommand) merge(request git2go.MergeCommand) (string, error) { repo, err := git2goutil.OpenRepository(request.Repository) if err != nil { return "", fmt.Errorf("could not open repository: %w", err) @@ -86,10 +85,14 @@ func merge(request git2go.MergeCommand) (string, error) { } } - tree, err := index.WriteTreeTo(repo) + treeOID, err := index.WriteTreeTo(repo) if err != nil { return "", fmt.Errorf("could not write tree: %w", err) } + tree, err := repo.LookupTree(treeOID) + if err != nil { + return "", fmt.Errorf("lookup tree: %w", err) + } author := git.Signature(git2go.NewSignature(request.AuthorName, request.AuthorMail, request.AuthorDate)) committer := author @@ -97,18 +100,20 @@ func merge(request git2go.MergeCommand) (string, error) { committer = git.Signature(git2go.NewSignature(request.CommitterName, request.CommitterMail, request.CommitterDate)) } - var parents []*git.Oid + var parents []*git.Commit if request.Squash { - parents = []*git.Oid{ours.Id()} + parents = []*git.Commit{ours} } else { - parents = []*git.Oid{ours.Id(), theirs.Id()} + parents = []*git.Commit{ours, theirs} } - commit, err := repo.CreateCommitFromIds("", &author, &committer, request.Message, tree, parents...) + + commitID, err := git2goutil.NewCommitSubmitter(repo, request.SigningKey). + Commit(&author, &committer, git.MessageEncodingUTF8, request.Message, tree, parents...) if err != nil { return "", fmt.Errorf("could not create merge commit: %w", err) } - return commit.String(), nil + return commitID.String(), nil } func resolveConflicts(repo *git.Repository, index *git.Index) error { diff --git a/cmd/gitaly-git2go/rebase.go b/cmd/gitaly-git2go/rebase.go index b0638c72e..1fa23984e 100644 --- a/cmd/gitaly-git2go/rebase.go +++ b/cmd/gitaly-git2go/rebase.go @@ -73,6 +73,7 @@ func (cmd *rebaseSubcommand) rebase(ctx context.Context, request *git2go.RebaseC return "", fmt.Errorf("get rebase options: %w", err) } opts.InMemory = 1 + opts.CommitCreateCallback = git2goutil.NewCommitSubmitter(repo, request.SigningKey).Commit var commit *git.AnnotatedCommit if request.BranchName != "" { diff --git a/cmd/gitaly-git2go/resolve_conflicts.go b/cmd/gitaly-git2go/resolve_conflicts.go index 73062eb17..6d05f59c7 100644 --- a/cmd/gitaly-git2go/resolve_conflicts.go +++ b/cmd/gitaly-git2go/resolve_conflicts.go @@ -180,26 +180,31 @@ func (cmd resolveSubcommand) Run(_ context.Context, decoder *gob.Decoder, encode return fmt.Errorf("Missing resolutions for the following files: %s", strings.Join(conflictPaths, ", ")) //nolint } - tree, err := index.WriteTreeTo(repo) + treeOID, err := index.WriteTreeTo(repo) if err != nil { return fmt.Errorf("write tree to repo: %w", err) } + tree, err := repo.LookupTree(treeOID) + if err != nil { + return fmt.Errorf("lookup tree: %w", err) + } - signature := git2go.NewSignature(request.AuthorName, request.AuthorMail, request.AuthorDate) + sign := git2go.NewSignature(request.AuthorName, request.AuthorMail, request.AuthorDate) committer := &git.Signature{ - Name: signature.Name, - Email: signature.Email, + Name: sign.Name, + Email: sign.Email, When: request.AuthorDate, } - commit, err := repo.CreateCommitFromIds("", committer, committer, request.Message, tree, ours.Id(), theirs.Id()) + commitID, err := git2goutil.NewCommitSubmitter(repo, request.SigningKey). + Commit(committer, committer, git.MessageEncodingUTF8, request.Message, tree, ours, theirs) if err != nil { - return fmt.Errorf("could not create resolve conflict commit: %w", err) + return fmt.Errorf("create commit: %w", err) } response := git2go.ResolveResult{ MergeResult: git2go.MergeResult{ - CommitID: commit.String(), + CommitID: commitID.String(), }, } diff --git a/cmd/gitaly-git2go/revert.go b/cmd/gitaly-git2go/revert.go index aaeeb00ac..725502289 100644 --- a/cmd/gitaly-git2go/revert.go +++ b/cmd/gitaly-git2go/revert.go @@ -85,20 +85,25 @@ func (cmd *revertSubcommand) revert(ctx context.Context, request *git2go.RevertC return "", git2go.HasConflictsError{} } - tree, err := index.WriteTreeTo(repo) + treeOID, err := index.WriteTreeTo(repo) if err != nil { return "", fmt.Errorf("write tree: %w", err) } + tree, err := repo.LookupTree(treeOID) + if err != nil { + return "", fmt.Errorf("lookup tree: %w", err) + } - if tree.Equal(ours.TreeId()) { + if treeOID.Equal(ours.TreeId()) { return "", git2go.EmptyError{} } committer := git.Signature(git2go.NewSignature(request.AuthorName, request.AuthorMail, request.AuthorDate)) - commit, err := repo.CreateCommitFromIds("", &committer, &committer, request.Message, tree, ours.Id()) + commitID, err := git2goutil.NewCommitSubmitter(repo, request.SigningKey). + Commit(&committer, &committer, git.MessageEncodingUTF8, request.Message, tree, ours) if err != nil { - return "", fmt.Errorf("create revert commit: %w", err) + return "", fmt.Errorf("create commit: %w", err) } - return commit.String(), nil + return commitID.String(), nil } diff --git a/cmd/gitaly-git2go/submodule.go b/cmd/gitaly-git2go/submodule.go index 0e52ee3f4..4596a5ff9 100644 --- a/cmd/gitaly-git2go/submodule.go +++ b/cmd/gitaly-git2go/submodule.go @@ -121,14 +121,16 @@ func (cmd *submoduleSubcommand) run(request git2go.SubmoduleCommand) (*git2go.Su request.AuthorDate, ), ) - newCommitOID, err := repo.CreateCommit( - "", // caller should update branch with hooks - &committer, - &committer, - request.Message, - newTree, - startCommit, - ) + + newCommitOID, err := git2goutil.NewCommitSubmitter(repo, request.SigningKey). + Commit( + &committer, + &committer, + git.MessageEncodingUTF8, + request.Message, + newTree, + startCommit, + ) if err != nil { return nil, fmt.Errorf( "%s: %w", diff --git a/go.mod b/go.mod index 57a4b2da3..c6cdf8a8e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitlab.com/gitlab-org/gitaly/v15 go 1.17 require ( + github.com/ProtonMail/go-crypto v0.0.0-20220810064516-de89276ce0f3 github.com/beevik/ntp v0.3.0 github.com/cloudflare/tableflip v1.2.3 github.com/containerd/cgroups v1.0.4 @@ -87,6 +88,7 @@ require ( github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/client9/reopen v1.0.0 // indirect + github.com/cloudflare/circl v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-minhash v0.0.0-20170608043002-7fe510aff544 // indirect diff --git a/go.sum b/go.sum index 50ebd8ac0..5cbd3509a 100644 --- a/go.sum +++ b/go.sum @@ -140,6 +140,8 @@ github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugX github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ProtonMail/go-crypto v0.0.0-20220810064516-de89276ce0f3 h1:JBPdE3yq6D8k8UxTzyCN2uhpHfGKsJEurKLHLU6YBdM= +github.com/ProtonMail/go-crypto v0.0.0-20220810064516-de89276ce0f3/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= @@ -216,6 +218,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -232,6 +235,8 @@ github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/reopen v1.0.0 h1:8tpLVR74DLpLObrn2KvsyxJY++2iORGR17WLUdSzUws= github.com/client9/reopen v1.0.0/go.mod h1:caXVCEr+lUtoN1FlsRiOWdfQtdRHIYfcb0ai8qKWtkQ= +github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/tableflip v1.2.3 h1:8I+B99QnnEWPHOY3fWipwVKxS70LGgUsslG7CSfmHMw= github.com/cloudflare/tableflip v1.2.3/go.mod h1:P4gRehmV6Z2bY5ao5ml9Pd8u6kuEnlB37pUFMmv7j2E= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= diff --git a/internal/backup/storage_service_sink.go b/internal/backup/storage_service_sink.go index 217fb322b..ef484624c 100644 --- a/internal/backup/storage_service_sink.go +++ b/internal/backup/storage_service_sink.go @@ -6,9 +6,9 @@ import ( "io" "gocloud.dev/blob" - _ "gocloud.dev/blob/azureblob" // nolint:nolintlint,golint,gci - _ "gocloud.dev/blob/gcsblob" // nolint:nolintlint,golint,gci - _ "gocloud.dev/blob/s3blob" // nolint:nolintlint,golint,gci + _ "gocloud.dev/blob/azureblob" //nolint:nolintlint,golint,gci + _ "gocloud.dev/blob/gcsblob" //nolint:nolintlint,golint,gci + _ "gocloud.dev/blob/s3blob" //nolint:nolintlint,golint,gci "gocloud.dev/gcerrors" ) diff --git a/internal/git/command_factory.go b/internal/git/command_factory.go index 060082ced..dae719935 100644 --- a/internal/git/command_factory.go +++ b/internal/git/command_factory.go @@ -257,7 +257,7 @@ func (cf *ExecCommandFactory) GetExecutionEnvironment(ctx context.Context) Execu } // If none is enabled though, we simply use the first execution environment, which is also - // the one with highest priority. This can for example happen in case we only were able to + // the one with the highest priority. This can for example happen in case we only were able to // construct a single execution environment that is currently feature flagged. return cf.execEnvs[0] } diff --git a/internal/git2go/apply.go b/internal/git2go/apply.go index 2bee12857..5c2b70da2 100644 --- a/internal/git2go/apply.go +++ b/internal/git2go/apply.go @@ -104,8 +104,13 @@ func (b *Executor) Apply(ctx context.Context, repo repository.GitRepo, params Ap execEnv := b.gitCmdFactory.GetExecutionEnvironment(ctx) + args := []string{"-git-binary-path", execEnv.BinaryPath} + if b.signingKey != "" { + args = append(args, "-signing-key", b.signingKey) + } + var result Result - output, err := b.run(ctx, repo, reader, "apply", "-git-binary-path", execEnv.BinaryPath) + output, err := b.run(ctx, repo, reader, "apply", args...) if err != nil { return "", fmt.Errorf("run: %w", err) } diff --git a/internal/git2go/apply_test.go b/internal/git2go/apply_test.go index 2888dab8b..af99a58e9 100644 --- a/internal/git2go/apply_test.go +++ b/internal/git2go/apply_test.go @@ -41,7 +41,7 @@ func TestExecutor_Apply(t *testing.T) { author := NewSignature("Test Author", "test.author@example.com", time.Now()) committer := NewSignature("Test Committer", "test.committer@example.com", time.Now()) - parentCommitSHA, err := executor.Commit(ctx, repo, CommitParams{ + parentCommitSHA, err := executor.Commit(ctx, repo, CommitCommand{ Repository: repoPath, Author: author, Committer: committer, @@ -50,7 +50,7 @@ func TestExecutor_Apply(t *testing.T) { }) require.NoError(t, err) - noCommonAncestor, err := executor.Commit(ctx, repo, CommitParams{ + noCommonAncestor, err := executor.Commit(ctx, repo, CommitCommand{ Repository: repoPath, Author: author, Committer: committer, @@ -59,7 +59,7 @@ func TestExecutor_Apply(t *testing.T) { }) require.NoError(t, err) - updateToA, err := executor.Commit(ctx, repo, CommitParams{ + updateToA, err := executor.Commit(ctx, repo, CommitCommand{ Repository: repoPath, Author: author, Committer: committer, @@ -69,7 +69,7 @@ func TestExecutor_Apply(t *testing.T) { }) require.NoError(t, err) - updateToB, err := executor.Commit(ctx, repo, CommitParams{ + updateToB, err := executor.Commit(ctx, repo, CommitCommand{ Repository: repoPath, Author: author, Committer: committer, @@ -79,7 +79,7 @@ func TestExecutor_Apply(t *testing.T) { }) require.NoError(t, err) - updateFromAToB, err := executor.Commit(ctx, repo, CommitParams{ + updateFromAToB, err := executor.Commit(ctx, repo, CommitCommand{ Repository: repoPath, Author: author, Committer: committer, @@ -89,7 +89,7 @@ func TestExecutor_Apply(t *testing.T) { }) require.NoError(t, err) - otherFile, err := executor.Commit(ctx, repo, CommitParams{ + otherFile, err := executor.Commit(ctx, repo, CommitCommand{ Repository: repoPath, Author: author, Committer: committer, @@ -214,7 +214,7 @@ func TestExecutor_Apply(t *testing.T) { Author: author, Committer: committer, Message: tc.patches[len(tc.patches)-1].Message, - }, getCommit(t, ctx, repo, commitID)) + }, getCommit(t, ctx, repo, commitID, false)) gittest.RequireTree(t, cfg, repoPath, commitID.String(), tc.tree) }) } diff --git a/internal/git2go/cherry_pick.go b/internal/git2go/cherry_pick.go index 6724d082a..7e954f50e 100644 --- a/internal/git2go/cherry_pick.go +++ b/internal/git2go/cherry_pick.go @@ -8,9 +8,9 @@ import ( "gitlab.com/gitlab-org/gitaly/v15/internal/git/repository" ) -// CherryPickCommand contains parameters to perform a cherry pick. +// CherryPickCommand contains parameters to perform a cherry-pick. type CherryPickCommand struct { - // Repository is the path where to execute the cherry pick. + // Repository is the path where to execute the cherry-pick. Repository string // CommitterName is the committer name for the resulting commit. CommitterName string @@ -26,9 +26,13 @@ type CherryPickCommand struct { Commit string // Mainline is the parent to be considered the mainline Mainline uint + // SigningKey is a path to the key to sign commit using OpenPGP + SigningKey string } -// CherryPick performs a cherry pick via gitaly-git2go. +// CherryPick performs a cherry-pick via gitaly-git2go. func (b *Executor) CherryPick(ctx context.Context, repo repository.GitRepo, m CherryPickCommand) (git.ObjectID, error) { + m.SigningKey = b.signingKey + return b.runWithGob(ctx, repo, "cherry-pick", m) } diff --git a/internal/git2go/commit.go b/internal/git2go/commit.go index 13edb4faa..7197e5a37 100644 --- a/internal/git2go/commit.go +++ b/internal/git2go/commit.go @@ -1,9 +1,7 @@ package git2go import ( - "bytes" "context" - "encoding/gob" "fmt" "gitlab.com/gitlab-org/gitaly/v15/internal/git" @@ -42,8 +40,8 @@ func (err DirectoryExistsError) Error() string { return fmt.Sprintf("directory exists: %q", string(err)) } -// CommitParams contains the information and the steps to build a commit. -type CommitParams struct { +// CommitCommand contains the information and the steps to build a commit. +type CommitCommand struct { // Repository is the path of the repository to operate on. Repository string // Author is the author of the commit. @@ -56,34 +54,14 @@ type CommitParams struct { Parent string // Actions are the steps to build the commit. Actions []Action + // SigningKey is a path to the key to sign commit using OpenPGP + SigningKey string } // Commit builds a commit from the actions, writes it to the object database and // returns its object id. -func (b *Executor) Commit(ctx context.Context, repo repository.GitRepo, params CommitParams) (git.ObjectID, error) { - input := &bytes.Buffer{} - if err := gob.NewEncoder(input).Encode(params); err != nil { - return "", err - } +func (b *Executor) Commit(ctx context.Context, repo repository.GitRepo, c CommitCommand) (git.ObjectID, error) { + c.SigningKey = b.signingKey - output, err := b.run(ctx, repo, input, "commit") - if err != nil { - return "", err - } - - var result Result - if err := gob.NewDecoder(output).Decode(&result); err != nil { - return "", err - } - - if result.Err != nil { - return "", result.Err - } - - commitID, err := git.ObjectHashSHA1.FromHex(result.CommitID) - if err != nil { - return "", fmt.Errorf("could not parse commit ID: %w", err) - } - - return commitID, nil + return b.runWithGob(ctx, repo, "commit", c) } diff --git a/internal/git2go/commit_test.go b/internal/git2go/commit_test.go index 0fef58310..48deee815 100644 --- a/internal/git2go/commit_test.go +++ b/internal/git2go/commit_test.go @@ -7,11 +7,14 @@ import ( "context" "errors" "fmt" + "os" "strconv" "strings" "testing" "time" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v15/internal/git" "gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest" @@ -63,8 +66,9 @@ func TestExecutor_Commit(t *testing.T) { executor := NewExecutor(cfg, gittest.NewCommandFactory(t, cfg), config.NewLocator(cfg)) for _, tc := range []struct { - desc string - steps []step + desc string + steps []step + signAndVerify bool }{ { desc: "create directory", @@ -455,14 +459,34 @@ func TestExecutor_Commit(t *testing.T) { }, }, }, + { + desc: "update created file, sign commit and verify signature", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file", OID: originalFile.String()}, + UpdateFile{Path: "file", OID: updatedFile.String()}, + }, + treeEntries: []gittest.TreeEntry{ + {Mode: DefaultMode, Path: "file", Content: "updated"}, + }, + }, + }, + signAndVerify: true, + }, } { t.Run(tc.desc, func(t *testing.T) { author := NewSignature("Author Name", "author.email@example.com", time.Now()) committer := NewSignature("Committer Name", "committer.email@example.com", time.Now()) + + if tc.signAndVerify { + executor.signingKey = testhelper.SigningKeyPath + } + var parentCommit git.ObjectID for i, step := range tc.steps { message := fmt.Sprintf("commit %d", i+1) - commitID, err := executor.Commit(ctx, repo, CommitParams{ + commitID, err := executor.Commit(ctx, repo, CommitCommand{ Repository: repoPath, Author: author, Committer: committer, @@ -483,7 +507,7 @@ func TestExecutor_Commit(t *testing.T) { Author: author, Committer: committer, Message: message, - }, getCommit(t, ctx, repo, commitID)) + }, getCommit(t, ctx, repo, commitID, tc.signAndVerify)) gittest.RequireTree(t, cfg, repoPath, commitID.String(), step.treeEntries) parentCommit = commitID @@ -492,24 +516,40 @@ func TestExecutor_Commit(t *testing.T) { } } -func getCommit(tb testing.TB, ctx context.Context, repo *localrepo.Repo, oid git.ObjectID) commit { +func getCommit(tb testing.TB, ctx context.Context, repo *localrepo.Repo, oid git.ObjectID, verifySignature bool) commit { tb.Helper() data, err := repo.ReadObject(ctx, oid) require.NoError(tb, err) + var ( + gpgsig, dataWithoutGpgSig string + gpgsigStarted bool + ) + var commit commit lines := strings.Split(string(data), "\n") for i, line := range lines { if line == "" { commit.Message = strings.Join(lines[i+1:], "\n") + dataWithoutGpgSig += "\n" + commit.Message break } + if gpgsigStarted && strings.HasPrefix(line, " ") { + gpgsig += strings.TrimSpace(line) + "\n" + continue + } + split := strings.SplitN(line, " ", 2) require.Len(tb, split, 2, "invalid commit: %q", data) field, value := split[0], split[1] + + if field != "gpgsig" { + dataWithoutGpgSig += line + "\n" + } + switch field { case "parent": require.Empty(tb, commit.Parent, "multi parent parsing not implemented") @@ -520,10 +560,29 @@ func getCommit(tb testing.TB, ctx context.Context, repo *localrepo.Repo, oid git case "committer": require.Empty(tb, commit.Committer, "commit contained multiple committers") commit.Committer = unmarshalSignature(tb, value) + case "gpgsig": + gpgsig = value + "\n" + gpgsigStarted = true default: } } + if gpgsig != "" || verifySignature { + file, err := os.Open("testdata/publicKey.gpg") + require.NoError(tb, err) + + keyring, err := openpgp.ReadKeyRing(file) + require.NoError(tb, err) + + _, err = openpgp.CheckArmoredDetachedSignature( + keyring, + strings.NewReader(dataWithoutGpgSig), + strings.NewReader(gpgsig), + &packet.Config{}, + ) + require.NoError(tb, err) + } + return commit } diff --git a/internal/git2go/executor.go b/internal/git2go/executor.go index 30c8ff0a3..b97a9f3b0 100644 --- a/internal/git2go/executor.go +++ b/internal/git2go/executor.go @@ -31,6 +31,7 @@ var ( // Executor executes gitaly-git2go. type Executor struct { binaryPath string + signingKey string gitCmdFactory git.CommandFactory locator storage.Locator logFormat, logLevel string @@ -41,6 +42,7 @@ type Executor struct { func NewExecutor(cfg config.Cfg, gitCmdFactory git.CommandFactory, locator storage.Locator) *Executor { return &Executor{ binaryPath: cfg.BinaryPath(BinaryName), + signingKey: cfg.Git.SigningKey, gitCmdFactory: gitCmdFactory, locator: locator, logFormat: cfg.Logging.Format, diff --git a/internal/git2go/merge.go b/internal/git2go/merge.go index 7086cfa76..8669a18eb 100644 --- a/internal/git2go/merge.go +++ b/internal/git2go/merge.go @@ -47,6 +47,8 @@ type MergeCommand struct { // If set to `true`, then the resulting commit will have `Ours` as its only parent. // Otherwise, a merge commit will be created with `Ours` and `Theirs` as its parents. Squash bool + // SigningKey is a path to the key to sign commit using OpenPGP + SigningKey string } // MergeResult contains results from a merge. @@ -60,6 +62,7 @@ func (b *Executor) Merge(ctx context.Context, repo repository.GitRepo, m MergeCo if err := m.verify(); err != nil { return MergeResult{}, fmt.Errorf("merge: %w: %s", ErrInvalidArgument, err.Error()) } + m.SigningKey = b.signingKey commitID, err := b.runWithGob(ctx, repo, "merge", m) if err != nil { diff --git a/internal/git2go/rebase.go b/internal/git2go/rebase.go index 9cd4ec814..8b041dadb 100644 --- a/internal/git2go/rebase.go +++ b/internal/git2go/rebase.go @@ -30,9 +30,13 @@ type RebaseCommand struct { // and which are thus empty to be skipped. If unset, empty commits will cause the rebase to // fail. SkipEmptyCommits bool + // SigningKey is a path to the key to sign commit using OpenPGP + SigningKey string } // Rebase performs the rebase via gitaly-git2go func (b *Executor) Rebase(ctx context.Context, repo repository.GitRepo, r RebaseCommand) (git.ObjectID, error) { + r.SigningKey = b.signingKey + return b.runWithGob(ctx, repo, "rebase", r) } diff --git a/internal/git2go/resolve_conflicts.go b/internal/git2go/resolve_conflicts.go index 69d8ed14f..cd1ad0d03 100644 --- a/internal/git2go/resolve_conflicts.go +++ b/internal/git2go/resolve_conflicts.go @@ -28,6 +28,8 @@ type ResolveResult struct { // Resolve will attempt merging and resolving conflicts for the provided request func (b *Executor) Resolve(ctx context.Context, repo repository.GitRepo, r ResolveCommand) (ResolveResult, error) { + r.SigningKey = b.signingKey + if err := r.verify(); err != nil { return ResolveResult{}, fmt.Errorf("resolve: %w: %s", ErrInvalidArgument, err.Error()) } diff --git a/internal/git2go/revert.go b/internal/git2go/revert.go index 7442e1566..4c42e706d 100644 --- a/internal/git2go/revert.go +++ b/internal/git2go/revert.go @@ -26,9 +26,13 @@ type RevertCommand struct { Revert string // Mainline is the parent to be considered the mainline Mainline uint + // SigningKey is a path to the key to sign commit using OpenPGP + SigningKey string } // Revert reverts a commit via gitaly-git2go. func (b *Executor) Revert(ctx context.Context, repo repository.GitRepo, r RevertCommand) (git.ObjectID, error) { + r.SigningKey = b.signingKey + return b.runWithGob(ctx, repo, "revert", r) } diff --git a/internal/git2go/submodule.go b/internal/git2go/submodule.go index e420ad5c5..ce4aad37f 100644 --- a/internal/git2go/submodule.go +++ b/internal/git2go/submodule.go @@ -37,6 +37,9 @@ type SubmoduleCommand struct { Submodule string // Branch where to commit submodule update Branch string + + // SigningKey is a path to the key to sign commit using OpenPGP + SigningKey string } // SubmoduleResult contains results from a committing a submodule update @@ -47,6 +50,8 @@ type SubmoduleResult struct { // Submodule attempts to commit the request submodule change func (b *Executor) Submodule(ctx context.Context, repo repository.GitRepo, s SubmoduleCommand) (SubmoduleResult, error) { + s.SigningKey = b.signingKey + if err := s.verify(); err != nil { return SubmoduleResult{}, fmt.Errorf("submodule: %w", err) } @@ -58,7 +63,7 @@ func (b *Executor) Submodule(ctx context.Context, repo repository.GitRepo, s Sub } // Ideally we would use `b.runWithGob` here to avoid the gob encoding - // boilerplate but it is not possible here because `runWithGob` adds error + // boilerplate, but it is not possible here because `runWithGob` adds error // prefixes and the `LegacyErrPrefix*` errors must match exactly. stdout, err := b.run(ctx, repo, input, cmd) if err != nil { diff --git a/internal/git2go/testdata/publicKey.gpg b/internal/git2go/testdata/publicKey.gpg new file mode 100644 index 000000000..98d02a71e Binary files /dev/null and b/internal/git2go/testdata/publicKey.gpg differ diff --git a/internal/git2go/testdata/signingKey.gpg b/internal/git2go/testdata/signingKey.gpg new file mode 100644 index 000000000..841fbc76a Binary files /dev/null and b/internal/git2go/testdata/signingKey.gpg differ diff --git a/internal/gitaly/config/config.go b/internal/gitaly/config/config.go index 4b816b752..f1aebe07d 100644 --- a/internal/gitaly/config/config.go +++ b/internal/gitaly/config/config.go @@ -108,6 +108,7 @@ type Git struct { CatfileCacheSize int `toml:"catfile_cache_size"` Config []GitConfig `toml:"config"` IgnoreGitconfig bool `toml:"ignore_gitconfig"` + SigningKey string `toml:"signing_key"` } // GitConfig contains a key-value pair which is to be passed to git as configuration. @@ -174,7 +175,7 @@ type StreamCacheConfig struct { } // Load initializes the Config variable from file and the environment. -// Environment variables take precedence over the file. +// Environment variables take precedence over the file. func Load(file io.Reader) (Cfg, error) { cfg := Cfg{ Prometheus: prometheus.DefaultConfig(), diff --git a/internal/gitaly/service/operations/apply_patch_test.go b/internal/gitaly/service/operations/apply_patch_test.go index 285977485..e1a38ab82 100644 --- a/internal/gitaly/service/operations/apply_patch_test.go +++ b/internal/gitaly/service/operations/apply_patch_test.go @@ -293,7 +293,7 @@ To restore the original branch and stop patching, run "git am --abort". var baseCommit git.ObjectID for _, action := range tc.baseCommit { var err error - baseCommit, err = executor.Commit(ctx, rewrittenRepo, git2go.CommitParams{ + baseCommit, err = executor.Commit(ctx, rewrittenRepo, git2go.CommitCommand{ Repository: repoPath, Author: author, Committer: committer, @@ -309,7 +309,7 @@ To restore the original branch and stop patching, run "git am --abort". } if tc.extraBranches != nil { - emptyCommit, err := executor.Commit(ctx, rewrittenRepo, git2go.CommitParams{ + emptyCommit, err := executor.Commit(ctx, rewrittenRepo, git2go.CommitCommand{ Repository: repoPath, Author: author, Committer: committer, @@ -329,7 +329,7 @@ To restore the original branch and stop patching, run "git am --abort". commit := baseCommit for _, action := range commitActions { var err error - commit, err = executor.Commit(ctx, rewrittenRepo, git2go.CommitParams{ + commit, err = executor.Commit(ctx, rewrittenRepo, git2go.CommitCommand{ Repository: repoPath, Author: author, Committer: committer, diff --git a/internal/gitaly/service/operations/commit_files.go b/internal/gitaly/service/operations/commit_files.go index 16df353e4..0637ee021 100644 --- a/internal/gitaly/service/operations/commit_files.go +++ b/internal/gitaly/service/operations/commit_files.go @@ -291,7 +291,7 @@ func (s *Server) userCommitFiles(ctx context.Context, header *gitalypb.UserCommi author = git2go.NewSignature(string(header.CommitAuthorName), string(header.CommitAuthorEmail), now) } - commitID, err := s.git2goExecutor.Commit(ctx, quarantineRepo, git2go.CommitParams{ + commitID, err := s.git2goExecutor.Commit(ctx, quarantineRepo, git2go.CommitCommand{ Repository: repoPath, Author: author, Committer: committer, diff --git a/internal/gitaly/service/remote/update_remote_mirror_test.go b/internal/gitaly/service/remote/update_remote_mirror_test.go index 8b74dd858..18f071787 100644 --- a/internal/gitaly/service/remote/update_remote_mirror_test.go +++ b/internal/gitaly/service/remote/update_remote_mirror_test.go @@ -560,7 +560,7 @@ func TestUpdateRemoteMirror(t *testing.T) { for _, commit := range commits { var err error commitOID, err = executor.Commit(ctx, gittest.RewrittenRepository(ctx, t, cfg, c.repoProto), - git2go.CommitParams{ + git2go.CommitCommand{ Repository: c.repoPath, Author: commitSignature, Committer: commitSignature, diff --git a/internal/testhelper/testhelper.go b/internal/testhelper/testhelper.go index f1db352f5..2599eed26 100644 --- a/internal/testhelper/testhelper.go +++ b/internal/testhelper/testhelper.go @@ -34,6 +34,8 @@ const ( RepositoryAuthToken = "the-secret-token" // DefaultStorageName is the default name of the Gitaly storage. DefaultStorageName = "default" + // SigningKeyPath is the default path to test commit signing. + SigningKeyPath = "testdata/signingKey.gpg" ) // IsPraefectEnabled returns whether this testing run is done with Praefect in front of the Gitaly. -- cgit v1.2.3