Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to use gpg signing for bootstrap commits #1854

Merged
merged 1 commit into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmd/flux/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ type bootstrapFlags struct {
authorName string
authorEmail string

gpgKeyPath string
gpgPassphrase string
gpgKeyID string

commitMessageAppendix string
}

Expand Down Expand Up @@ -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())
Expand Down
1 change: 1 addition & 0 deletions cmd/flux/bootstrap_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions internal/bootstrap/bootstrap_plain_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ type PlainGitBootstrapper struct {
author git.Author
commitMessageAppendix string

gpgKeyPath string
gpgPassphrase string
gpgKeyID string

kubeconfig string
kubecontext string

Expand Down Expand Up @@ -142,14 +146,15 @@ 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
}
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)
}
Expand Down Expand Up @@ -306,14 +311,16 @@ 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
}
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)
}
Expand Down
37 changes: 37 additions & 0 deletions internal/bootstrap/git/commit_options.go
Original file line number Diff line number Diff line change
@@ -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,
},
}
}
2 changes: 1 addition & 1 deletion internal/bootstrap/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
68 changes: 65 additions & 3 deletions internal/bootstrap/git/gogit/gogit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
66 changes: 66 additions & 0 deletions internal/bootstrap/git/gogit/gogit_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
Binary file added internal/bootstrap/git/gogit/testdata/private.key
Binary file not shown.
24 changes: 24 additions & 0 deletions internal/bootstrap/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}