From 0beab87f5b73893f2a394def51e12de099aa757e Mon Sep 17 00:00:00 2001 From: Somtochi Onyekwere Date: Thu, 23 Sep 2021 09:29:21 +0100 Subject: [PATCH] Add gpg key path and passphrase as args Signed-off-by: Somtochi Onyekwere --- cmd/flux/bootstrap.go | 8 +++ cmd/flux/bootstrap_git.go | 1 + go.mod | 1 + internal/bootstrap/bootstrap_plain_git.go | 11 ++- internal/bootstrap/git/commit_options.go | 37 ++++++++++ internal/bootstrap/git/git.go | 2 +- internal/bootstrap/git/gogit/gogit.go | 68 +++++++++++++++++- internal/bootstrap/git/gogit/gogit_test.go | 66 +++++++++++++++++ .../bootstrap/git/gogit/testdata/private.key | Bin 0 -> 7533 bytes internal/bootstrap/options.go | 24 +++++++ 10 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 internal/bootstrap/git/commit_options.go create mode 100644 internal/bootstrap/git/gogit/gogit_test.go create mode 100644 internal/bootstrap/git/gogit/testdata/private.key diff --git a/cmd/flux/bootstrap.go b/cmd/flux/bootstrap.go index b2359958..2abfc9ff 100644 --- a/cmd/flux/bootstrap.go +++ b/cmd/flux/bootstrap.go @@ -68,6 +68,10 @@ type bootstrapFlags struct { authorName string authorEmail string + gpgKeyPath string + gpgPassphrase string + gpgKeyID string + commitMessageAppendix string } @@ -119,6 +123,10 @@ func init() { bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorName, "author-name", "Flux", "author name for Git commits") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorEmail, "author-email", "", "author email for Git commits") + bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyPath, "gpg-key", "", "path to secret gpg key for signing commits") + bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgPassphrase, "gpg-passphrase", "", "passphrase for decrypting secret gpg key") + bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyID, "gpg-key-id", "", "key id for selecting a particular key") + bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'") bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.arch, "arch", bootstrapArgs.arch.Description()) diff --git a/cmd/flux/bootstrap_git.go b/cmd/flux/bootstrap_git.go index f01d925c..894742c4 100644 --- a/cmd/flux/bootstrap_git.go +++ b/cmd/flux/bootstrap_git.go @@ -224,6 +224,7 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { bootstrap.WithPostGenerateSecretFunc(promptPublicKey), bootstrap.WithLogger(logger), bootstrap.WithCABundle(caBundle), + bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } // Setup bootstrapper with constructed configs diff --git a/go.mod b/go.mod index f03e0b04..2c03bc94 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/Masterminds/semver/v3 v3.1.0 + github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 github.com/cyphar/filepath-securejoin v0.2.2 github.com/fluxcd/go-git-providers v0.1.1 github.com/fluxcd/helm-controller/api v0.11.2 diff --git a/internal/bootstrap/bootstrap_plain_git.go b/internal/bootstrap/bootstrap_plain_git.go index 6475c40f..898f976d 100644 --- a/internal/bootstrap/bootstrap_plain_git.go +++ b/internal/bootstrap/bootstrap_plain_git.go @@ -53,6 +53,10 @@ type PlainGitBootstrapper struct { author git.Author commitMessageAppendix string + gpgKeyPath string + gpgPassphrase string + gpgKeyID string + kubeconfig string kubecontext string @@ -142,6 +146,7 @@ func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifest } // Git commit generated + gpgOpts := git.WithGpgSigningOption(b.gpgKeyPath, b.gpgPassphrase, b.gpgKeyID) commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version) if b.commitMessageAppendix != "" { commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix @@ -149,7 +154,7 @@ func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifest commit, err := b.git.Commit(git.Commit{ Author: b.author, Message: commitMsg, - }) + }, gpgOpts) if err != nil && err != git.ErrNoStagedFiles { return fmt.Errorf("failed to commit sync manifests: %w", err) } @@ -306,6 +311,7 @@ func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options b.logger.Successf("generated sync manifests") // Git commit generated + gpgOpts := git.WithGpgSigningOption(b.gpgKeyPath, b.gpgPassphrase, b.gpgKeyID) commitMsg := fmt.Sprintf("Add Flux sync manifests") if b.commitMessageAppendix != "" { commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix @@ -313,7 +319,8 @@ func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options commit, err := b.git.Commit(git.Commit{ Author: b.author, Message: commitMsg, - }) + }, gpgOpts) + if err != nil && err != git.ErrNoStagedFiles { return fmt.Errorf("failed to commit sync manifests: %w", err) } diff --git a/internal/bootstrap/git/commit_options.go b/internal/bootstrap/git/commit_options.go new file mode 100644 index 00000000..e1a9712c --- /dev/null +++ b/internal/bootstrap/git/commit_options.go @@ -0,0 +1,37 @@ +package git + +// Option is a some configuration that modifies options for a commit. +type Option interface { + // ApplyToCommit applies this configuration to a given commit option. + ApplyToCommit(*CommitOptions) +} + +// CommitOptions contains options for making a commit. +type CommitOptions struct { + *GPGSigningInfo +} + +// GPGSigningInfo contains information for signing a commit. +type GPGSigningInfo struct { + PrivateKeyPath string + Passphrase string + KeyID string +} + +type GpgSigningOption struct { + *GPGSigningInfo +} + +func (w GpgSigningOption) ApplyToCommit(in *CommitOptions) { + in.GPGSigningInfo = w.GPGSigningInfo +} + +func WithGpgSigningOption(path, passphrase, keyID string) Option { + return GpgSigningOption{ + GPGSigningInfo: &GPGSigningInfo{ + PrivateKeyPath: path, + Passphrase: passphrase, + KeyID: keyID, + }, + } +} diff --git a/internal/bootstrap/git/git.go b/internal/bootstrap/git/git.go index 103ea49b..1a0f6d86 100644 --- a/internal/bootstrap/git/git.go +++ b/internal/bootstrap/git/git.go @@ -44,7 +44,7 @@ type Git interface { Init(url, branch string) (bool, error) Clone(ctx context.Context, url, branch string, caBundle []byte) (bool, error) Write(path string, reader io.Reader) error - Commit(message Commit) (string, error) + Commit(message Commit, options ...Option) (string, error) Push(ctx context.Context, caBundle []byte) error Status() (bool, error) Head() (string, error) diff --git a/internal/bootstrap/git/gogit/gogit.go b/internal/bootstrap/git/gogit/gogit.go index 07248791..333661d5 100644 --- a/internal/bootstrap/git/gogit/gogit.go +++ b/internal/bootstrap/git/gogit/gogit.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "github.com/ProtonMail/go-crypto/openpgp" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" @@ -40,6 +41,12 @@ type GoGit struct { repository *gogit.Repository } +type CommitOptions struct { + GpgKeyPath string + GpgKeyPassphrase string + KeyID string +} + func New(path string, auth transport.AuthMethod) *GoGit { return &GoGit{ path: path, @@ -127,7 +134,7 @@ func (g *GoGit) Write(path string, reader io.Reader) error { return err } -func (g *GoGit) Commit(message git.Commit) (string, error) { +func (g *GoGit) Commit(message git.Commit, opts ...git.Option) (string, error) { if g.repository == nil { return "", git.ErrNoGitRepository } @@ -142,6 +149,12 @@ func (g *GoGit) Commit(message git.Commit) (string, error) { return "", err } + // apply the options + options := &git.CommitOptions{} + for _, opt := range opts { + opt.ApplyToCommit(options) + } + // go-git has [a bug](https://github.com/go-git/go-git/issues/253) // whereby it thinks broken symlinks to absolute paths are // modified. There's no circumstance in which we want to commit a @@ -173,13 +186,24 @@ func (g *GoGit) Commit(message git.Commit) (string, error) { return head.Hash().String(), git.ErrNoStagedFiles } - commit, err := wt.Commit(message.Message, &gogit.CommitOptions{ + commitOpts := &gogit.CommitOptions{ Author: &object.Signature{ Name: message.Name, Email: message.Email, When: time.Now(), }, - }) + } + + if options.GPGSigningInfo != nil { + entity, err := getOpenPgpEntity(*options.GPGSigningInfo) + if err != nil { + return "", err + } + + commitOpts.SignKey = entity + } + + commit, err := wt.Commit(message.Message, commitOpts) if err != nil { return "", err } @@ -232,3 +256,41 @@ func (g *GoGit) Path() string { func isRemoteBranchNotFoundErr(err error, ref string) bool { return strings.Contains(err.Error(), fmt.Sprintf("couldn't find remote ref %q", ref)) } + +func getOpenPgpEntity(info git.GPGSigningInfo) (*openpgp.Entity, error) { + r, err := os.Open(info.PrivateKeyPath) + if err != nil { + return nil, err + } + + entityList, err := openpgp.ReadKeyRing(r) + if err != nil { + return nil, err + } + + if len(entityList) == 0 { + return nil, fmt.Errorf("no entity formed") + } + + var entity *openpgp.Entity + if info.KeyID != "" { + for _, ent := range entityList { + if ent.PrimaryKey.KeyIdString() == info.KeyID { + entity = ent + } + } + + if entity == nil { + return nil, fmt.Errorf("no key matching the key id was found") + } + } else { + entity = entityList[0] + } + + err = entity.PrivateKey.Decrypt([]byte(info.Passphrase)) + if err != nil { + return nil, err + } + + return entity, nil +} diff --git a/internal/bootstrap/git/gogit/gogit_test.go b/internal/bootstrap/git/gogit/gogit_test.go new file mode 100644 index 00000000..1c8bb311 --- /dev/null +++ b/internal/bootstrap/git/gogit/gogit_test.go @@ -0,0 +1,66 @@ +// +build unit + +package gogit + +import ( + "testing" + + "github.com/fluxcd/flux2/internal/bootstrap/git" +) + +func TestGetOpenPgpEntity(t *testing.T) { + tests := []struct { + name string + keyPath string + passphrase string + id string + expectErr bool + }{ + { + name: "no default key id given", + keyPath: "testdata/private.key", + passphrase: "flux", + id: "", + expectErr: false, + }, + { + name: "key id given", + keyPath: "testdata/private.key", + passphrase: "flux", + id: "0619327DBD777415", + expectErr: false, + }, + { + name: "wrong key id", + keyPath: "testdata/private.key", + passphrase: "flux", + id: "0619327DBD777416", + expectErr: true, + }, + { + name: "wrong password", + keyPath: "testdata/private.key", + passphrase: "fluxe", + id: "0619327DBD777415", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gpgInfo := git.GPGSigningInfo{ + PrivateKeyPath: tt.keyPath, + Passphrase: tt.passphrase, + KeyID: tt.id, + } + + _, err := getOpenPgpEntity(gpgInfo) + if err != nil && !tt.expectErr { + t.Errorf("unexpected error: %s", err) + } + if err == nil && tt.expectErr { + t.Errorf("expected error when %s", tt.name) + } + }) + } +} diff --git a/internal/bootstrap/git/gogit/testdata/private.key b/internal/bootstrap/git/gogit/testdata/private.key new file mode 100644 index 0000000000000000000000000000000000000000..9e56432c627b84d068b2fe3f9abd07a8fcb5368a GIT binary patch literal 7533 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 literal 0 HcmV?d00001 diff --git a/internal/bootstrap/options.go b/internal/bootstrap/options.go index 88972c1e..750f66e9 100644 --- a/internal/bootstrap/options.go +++ b/internal/bootstrap/options.go @@ -112,3 +112,27 @@ func (o loggerOption) applyGit(b *PlainGitBootstrapper) { func (o loggerOption) applyGitProvider(b *GitProviderBootstrapper) { b.logger = o.logger } + +func WithGitCommitSigning(path, passphrase, keyID string) Option { + return gitCommitSigningOption{ + gpgKeyPath: path, + gpgPassphrase: passphrase, + gpgKeyID: keyID, + } +} + +type gitCommitSigningOption struct { + gpgKeyPath string + gpgPassphrase string + gpgKeyID string +} + +func (o gitCommitSigningOption) applyGit(b *PlainGitBootstrapper) { + b.gpgPassphrase = o.gpgPassphrase + b.gpgKeyPath = o.gpgKeyPath + b.gpgKeyID = o.gpgKeyID +} + +func (o gitCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) { + o.applyGit(b.PlainGitBootstrapper) +}