Skip to content

Commit

Permalink
fix: Correctly verify signatures when targetRevision is a branch name (
Browse files Browse the repository at this point in the history
…argoproj#14214)

* fix: Correctly verify signatures when targetRevision is a branch name

Signed-off-by: jannfis <jann@mistrust.net>

* Add more e2e tests

Signed-off-by: jannfis <jann@mistrust.net>

* Fix a bug and add unit test

Signed-off-by: jannfis <jann@mistrust.net>

---------

Signed-off-by: jannfis <jann@mistrust.net>
  • Loading branch information
jannfis committed Jun 27, 2023
1 parent 4761611 commit 5d70a78
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 60 deletions.
32 changes: 16 additions & 16 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ require (
github.com/antonmedv/expr v1.9.0
github.com/argoproj/gitops-engine v0.7.1-0.20230512020822-b4dd8b8c3976
github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da
github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d
github.com/aws/aws-sdk-go v1.44.164
github.com/argoproj/pkg v0.13.7-0.20230627120311-a4dd357b057e
github.com/aws/aws-sdk-go v1.44.290
github.com/bombsimon/logrusr/v2 v2.0.1
github.com/bradleyfalzon/ghinstallation/v2 v2.1.0
github.com/casbin/casbin/v2 v2.60.0
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/cyphar/filepath-securejoin v0.2.3
github.com/dustin/go-humanize v1.0.0
github.com/dustin/go-humanize v1.0.1
github.com/evanphx/json-patch v5.6.0+incompatible
github.com/fsnotify/fsnotify v1.5.1
github.com/ghodss/yaml v1.0.0
Expand Down Expand Up @@ -65,21 +65,21 @@ require (
github.com/prometheus/client_golang v1.14.0
github.com/r3labs/diff v1.1.0
github.com/rs/cors v1.8.0 // indirect
github.com/sirupsen/logrus v1.9.0
github.com/sirupsen/logrus v1.9.3
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c
github.com/soheilhy/cmux v0.1.5
github.com/spf13/cobra v1.6.1
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
github.com/stretchr/testify v1.8.4
github.com/valyala/fasttemplate v1.2.2
github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0
github.com/xanzy/go-gitlab v0.60.0
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64
golang.org/x/crypto v0.3.0
golang.org/x/net v0.7.0 // indirect
golang.org/x/crypto v0.10.0
golang.org/x/net v0.11.0 // indirect
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
golang.org/x/term v0.5.0
golang.org/x/sync v0.1.0
golang.org/x/term v0.9.0
google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368
google.golang.org/grpc v1.51.0
google.golang.org/protobuf v1.28.1
Expand Down Expand Up @@ -181,15 +181,15 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-version v1.2.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/timefmt-go v0.1.4 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
Expand Down Expand Up @@ -230,11 +230,11 @@ require (
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
golang.org/x/exp v0.0.0-20210901193431-a062eea981d2 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/tools v0.6.0 // indirect
gomodules.xyz/envconfig v1.3.1-0.20190308184047-426f31af0d45 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
gomodules.xyz/notify v0.1.1 // indirect
Expand Down
75 changes: 45 additions & 30 deletions go.sum

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion reposerver/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,16 @@ func (s *Service) runRepoOperation(
return operation(gitClient.Root(), commitSHA, revision, func() (*operationContext, error) {
var signature string
if verifyCommit {
signature, err = gitClient.VerifyCommitSignature(unresolvedRevision)
// When the revision is an annotated tag, we need to pass the unresolved revision (i.e. the tag name)
// to the verification routine. For everything else, we work with the SHA that the target revision is
// pointing to (i.e. the resolved revision).
var rev string
if gitClient.IsAnnotatedTag(revision) {
rev = unresolvedRevision
} else {
rev = revision
}
signature, err = gitClient.VerifyCommitSignature(rev)
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions reposerver/repository/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client)
gitClient.On("LsRemote", mock.Anything).Return(mock.Anything, nil)
gitClient.On("CommitSHA").Return(mock.Anything, nil)
gitClient.On("Root").Return(root)
gitClient.On("IsAnnotatedTag").Return(false)
if signed {
gitClient.On("VerifyCommitSignature", mock.Anything).Return(testSignature, nil)
} else {
Expand Down
55 changes: 55 additions & 0 deletions test/e2e/app_management_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,61 @@ func TestSyncToSignedCommitWithKnownKey(t *testing.T) {
Expect(HealthIs(health.HealthStatusHealthy))
}

func TestSyncToSignedBranchWithKnownKey(t *testing.T) {
SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Path(guestbookPath).
Revision("master").
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy))
}

func TestSyncToSignedBranchWithUnknownKey(t *testing.T) {
SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Path(guestbookPath).
Revision("master").
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}

func TestSyncToUnsignedBranch(t *testing.T) {
SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Revision("master").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}

func TestSyncToSignedTagWithKnownKey(t *testing.T) {
SkipOnEnv(t, "GPG")
Given(t).
Expand Down
6 changes: 5 additions & 1 deletion util/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ type ExecRunOpts struct {
Redactor func(text string) string
// TimeoutBehavior configures what to do in case of timeout
TimeoutBehavior argoexec.TimeoutBehavior
// SkipErrorLogging determines whether to skip logging of execution errors (rc > 0)
SkipErrorLogging bool
// CaptureStderr determines whether to capture stderr in addition to stdout
CaptureStderr bool
}

func init() {
Expand All @@ -43,7 +47,7 @@ func RunWithRedactor(cmd *exec.Cmd, redactor func(text string) string) (string,
}

func RunWithExecRunOpts(cmd *exec.Cmd, opts ExecRunOpts) (string, error) {
cmdOpts := argoexec.CmdOpts{Timeout: timeout, Redactor: opts.Redactor, TimeoutBehavior: opts.TimeoutBehavior}
cmdOpts := argoexec.CmdOpts{Timeout: timeout, Redactor: opts.Redactor, TimeoutBehavior: opts.TimeoutBehavior, SkipErrorLogging: opts.SkipErrorLogging}
span := tracing.NewLoggingTracer(log.NewLogrusLogger(log.NewWithCurrentConfig())).StartSpan(fmt.Sprintf("exec %v", cmd.Args[0]))
span.SetBaggageItem("dir", fmt.Sprintf("%v", cmd.Dir))
if cmdOpts.Redactor != nil {
Expand Down
27 changes: 23 additions & 4 deletions util/git/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ type Client interface {
CommitSHA() (string, error)
RevisionMetadata(revision string) (*RevisionMetadata, error)
VerifyCommitSignature(string) (string, error)
IsAnnotatedTag(string) bool
}

type EventHandlers struct {
Expand Down Expand Up @@ -100,6 +101,11 @@ type nativeGitClient struct {
proxy string
}

type runOpts struct {
SkipErrorLogging bool
CaptureStderr bool
}

var (
maxAttemptsCount = 1
maxRetryDuration time.Duration
Expand Down Expand Up @@ -617,17 +623,28 @@ func (m *nativeGitClient) VerifyCommitSignature(revision string) (string, error)
return out, nil
}

// IsAnnotatedTag returns true if the revision points to an annotated tag
func (m *nativeGitClient) IsAnnotatedTag(revision string) bool {
cmd := exec.Command("git", "describe", "--exact-match", revision)
out, err := m.runCmdOutput(cmd, runOpts{SkipErrorLogging: true})
if out != "" && err == nil {
return true
} else {
return false
}
}

// runWrapper runs a custom command with all the semantics of running the Git client
func (m *nativeGitClient) runGnuPGWrapper(wrapper string, args ...string) (string, error) {
cmd := exec.Command(wrapper, args...)
cmd.Env = append(cmd.Env, fmt.Sprintf("GNUPGHOME=%s", common.GetGnuPGHomePath()), "LANG=C")
return m.runCmdOutput(cmd)
return m.runCmdOutput(cmd, runOpts{})
}

// runCmd is a convenience function to run a command in a given directory and return its output
func (m *nativeGitClient) runCmd(args ...string) (string, error) {
cmd := exec.Command("git", args...)
return m.runCmdOutput(cmd)
return m.runCmdOutput(cmd, runOpts{})
}

// runCredentialedCmd is a convenience function to run a git command with username/password credentials
Expand All @@ -649,11 +666,11 @@ func (m *nativeGitClient) runCredentialedCmd(command string, args ...string) err

cmd := exec.Command(command, args...)
cmd.Env = append(cmd.Env, environ...)
_, err = m.runCmdOutput(cmd)
_, err = m.runCmdOutput(cmd, runOpts{})
return err
}

func (m *nativeGitClient) runCmdOutput(cmd *exec.Cmd) (string, error) {
func (m *nativeGitClient) runCmdOutput(cmd *exec.Cmd, ropts runOpts) (string, error) {
cmd.Dir = m.root
cmd.Env = append(os.Environ(), cmd.Env...)
// Set $HOME to nowhere, so we can be execute Git regardless of any external
Expand Down Expand Up @@ -689,6 +706,8 @@ func (m *nativeGitClient) runCmdOutput(cmd *exec.Cmd) (string, error) {
Signal: syscall.SIGTERM,
ShouldWait: true,
},
SkipErrorLogging: ropts.SkipErrorLogging,
CaptureStderr: ropts.CaptureStderr,
}
return executil.RunWithExecRunOpts(cmd, opts)
}
45 changes: 45 additions & 0 deletions util/git/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"testing"

Expand Down Expand Up @@ -70,6 +71,50 @@ func Test_nativeGitClient_Fetch_Prune(t *testing.T) {
assert.NoError(t, err)
}

func Test_IsAnnotatedTag(t *testing.T) {
tempDir := t.TempDir()
client, err := NewClient(fmt.Sprintf("file://%s", tempDir), NopCreds{}, true, false, "")
require.NoError(t, err)

err = client.Init()
require.NoError(t, err)

p := path.Join(client.Root(), "README")
f, err := os.Create(p)
require.NoError(t, err)
_, err = f.WriteString("Hello.")
require.NoError(t, err)
err = f.Close()
require.NoError(t, err)

err = runCmd(client.Root(), "git", "add", "README")
require.NoError(t, err)

err = runCmd(client.Root(), "git", "commit", "-m", "Initial commit", "-a")
require.NoError(t, err)

atag := client.IsAnnotatedTag("master")
assert.False(t, atag)

err = runCmd(client.Root(), "git", "tag", "some-tag", "-a", "-m", "Create annotated tag")
require.NoError(t, err)
atag = client.IsAnnotatedTag("some-tag")
assert.True(t, atag)

// Tag effectually points to HEAD, so it's considered the same
atag = client.IsAnnotatedTag("HEAD")
assert.True(t, atag)

err = runCmd(client.Root(), "git", "rm", "README")
assert.NoError(t, err)
err = runCmd(client.Root(), "git", "commit", "-m", "remove README", "-a")
assert.NoError(t, err)

// We moved on, so tag doesn't point to HEAD anymore
atag = client.IsAnnotatedTag("HEAD")
assert.False(t, atag)
}

func Test_nativeGitClient_Submodule(t *testing.T) {
tempDir, err := os.MkdirTemp("", "")
require.NoError(t, err)
Expand Down
Loading

0 comments on commit 5d70a78

Please sign in to comment.