diff --git a/cmd/flux/bootstrap_bitbucket_server.go b/cmd/flux/bootstrap_bitbucket_server.go index be8e004517..982f6d9fa5 100644 --- a/cmd/flux/bootstrap_bitbucket_server.go +++ b/cmd/flux/bootstrap_bitbucket_server.go @@ -22,6 +22,7 @@ import ( "os" "time" + "github.com/ProtonMail/go-crypto/openpgp" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/spf13/cobra" @@ -212,19 +213,18 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { secretOpts.Username = bServerArgs.username } secretOpts.Password = bitbucketToken - - if bootstrapArgs.caFile != "" { - secretOpts.CAFilePath = bootstrapArgs.caFile - } + secretOpts.CAFile = caBundle } else { + keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password) + if err != nil { + return err + } + secretOpts.Keypair = keypair secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve - secretOpts.SSHHostname = bServerArgs.hostname - if bootstrapArgs.privateKeyFile != "" { - secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile - } + secretOpts.SSHHostname = bServerArgs.hostname if bootstrapArgs.sshHostname != "" { secretOpts.SSHHostname = bootstrapArgs.sshHostname } @@ -243,7 +243,21 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { RecurseSubmodules: bootstrapArgs.recurseSubmodules, } + // Read PGP Key + var entityList openpgp.EntityList + if bootstrapArgs.gpgKeyRingPath != "" { + r, err := os.Open(bootstrapArgs.gpgKeyRingPath) + if err != nil { + return fmt.Errorf("unable to open GPG key ring: %w", err) + } + entityList, err = openpgp.ReadKeyRing(r) + if err != nil { + return err + } + } + // Bootstrap config + bootstrapOpts := []bootstrap.GitProviderOption{ bootstrap.WithProviderRepository(bServerArgs.owner, bServerArgs.repository, bServerArgs.personal), bootstrap.WithBranch(bootstrapArgs.branch), @@ -255,7 +269,7 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithLogger(logger), bootstrap.WithCABundle(caBundle), - bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyRingPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), + bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) diff --git a/cmd/flux/bootstrap_git.go b/cmd/flux/bootstrap_git.go index e0da5bb443..d9a14cae23 100644 --- a/cmd/flux/bootstrap_git.go +++ b/cmd/flux/bootstrap_git.go @@ -24,6 +24,7 @@ import ( "strings" "time" + "github.com/ProtonMail/go-crypto/openpgp" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/plumbing/transport/ssh" @@ -169,6 +170,15 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { installOptions.BaseURL = customBaseURL } + var caBundle []byte + if bootstrapArgs.caFile != "" { + var err error + caBundle, err = os.ReadFile(bootstrapArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + } + // Source generation and secret config secretOpts := sourcesecret.Options{ Name: bootstrapArgs.secretName, @@ -179,10 +189,7 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { if bootstrapArgs.tokenAuth { secretOpts.Username = gitArgs.username secretOpts.Password = gitArgs.password - - if bootstrapArgs.caFile != "" { - secretOpts.CAFilePath = bootstrapArgs.caFile - } + secretOpts.CAFile = caBundle // Remove port of the given host when not syncing over HTTP/S to not assume port for protocol // This _might_ be overwritten later on by e.g. --ssh-hostname @@ -213,9 +220,12 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { if bootstrapArgs.sshHostname != "" { repositoryURL.Host = bootstrapArgs.sshHostname } - if bootstrapArgs.privateKeyFile != "" { - secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile + + keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password) + if err != nil { + return err } + secretOpts.Keypair = keypair // Configure last as it depends on the config above. secretOpts.SSHHostname = repositoryURL.Host @@ -235,12 +245,16 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { RecurseSubmodules: bootstrapArgs.recurseSubmodules, } - var caBundle []byte - if bootstrapArgs.caFile != "" { - var err error - caBundle, err = os.ReadFile(bootstrapArgs.caFile) + // Read PGP Key + var entityList openpgp.EntityList + if bootstrapArgs.gpgKeyRingPath != "" { + r, err := os.Open(bootstrapArgs.gpgKeyRingPath) if err != nil { - return fmt.Errorf("unable to read TLS CA file: %w", err) + return fmt.Errorf("unable to open GPG key ring: %w", err) + } + entityList, err = openpgp.ReadKeyRing(r) + if err != nil { + return err } } @@ -254,7 +268,7 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { bootstrap.WithPostGenerateSecretFunc(promptPublicKey), bootstrap.WithLogger(logger), bootstrap.WithCABundle(caBundle), - bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyRingPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), + bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } // Setup bootstrapper with constructed configs diff --git a/cmd/flux/bootstrap_github.go b/cmd/flux/bootstrap_github.go index c49bd483e4..002c790366 100644 --- a/cmd/flux/bootstrap_github.go +++ b/cmd/flux/bootstrap_github.go @@ -22,6 +22,7 @@ import ( "os" "time" + "github.com/ProtonMail/go-crypto/openpgp" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/spf13/cobra" @@ -204,16 +205,13 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { if bootstrapArgs.tokenAuth { secretOpts.Username = "git" secretOpts.Password = ghToken - - if bootstrapArgs.caFile != "" { - secretOpts.CAFilePath = bootstrapArgs.caFile - } + secretOpts.CAFile = caBundle } else { secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve - secretOpts.SSHHostname = githubArgs.hostname + secretOpts.SSHHostname = githubArgs.hostname if bootstrapArgs.sshHostname != "" { secretOpts.SSHHostname = bootstrapArgs.sshHostname } @@ -232,6 +230,19 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { RecurseSubmodules: bootstrapArgs.recurseSubmodules, } + // Read PGP Key + var entityList openpgp.EntityList + if bootstrapArgs.gpgKeyRingPath != "" { + r, err := os.Open(bootstrapArgs.gpgKeyRingPath) + if err != nil { + return fmt.Errorf("unable to open GPG key ring: %w", err) + } + entityList, err = openpgp.ReadKeyRing(r) + if err != nil { + return err + } + } + // Bootstrap config bootstrapOpts := []bootstrap.GitProviderOption{ bootstrap.WithProviderRepository(githubArgs.owner, githubArgs.repository, githubArgs.personal), @@ -244,7 +255,7 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithLogger(logger), bootstrap.WithCABundle(caBundle), - bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyRingPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), + bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) diff --git a/cmd/flux/bootstrap_gitlab.go b/cmd/flux/bootstrap_gitlab.go index 86f498c6c3..936afc43fc 100644 --- a/cmd/flux/bootstrap_gitlab.go +++ b/cmd/flux/bootstrap_gitlab.go @@ -24,6 +24,7 @@ import ( "strings" "time" + "github.com/ProtonMail/go-crypto/openpgp" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/spf13/cobra" @@ -215,19 +216,18 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { if bootstrapArgs.tokenAuth { secretOpts.Username = "git" secretOpts.Password = glToken - - if bootstrapArgs.caFile != "" { - secretOpts.CAFilePath = bootstrapArgs.caFile - } + secretOpts.CAFile = caBundle } else { + keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password) + if err != nil { + return err + } + secretOpts.Keypair = keypair secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve - secretOpts.SSHHostname = gitlabArgs.hostname - if bootstrapArgs.privateKeyFile != "" { - secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile - } + secretOpts.SSHHostname = gitlabArgs.hostname if bootstrapArgs.sshHostname != "" { secretOpts.SSHHostname = bootstrapArgs.sshHostname } @@ -246,6 +246,19 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { RecurseSubmodules: bootstrapArgs.recurseSubmodules, } + // Read PGP Key + var entityList openpgp.EntityList + if bootstrapArgs.gpgKeyRingPath != "" { + r, err := os.Open(bootstrapArgs.gpgKeyRingPath) + if err != nil { + return fmt.Errorf("unable to open GPG key ring: %w", err) + } + entityList, err = openpgp.ReadKeyRing(r) + if err != nil { + return err + } + } + // Bootstrap config bootstrapOpts := []bootstrap.GitProviderOption{ bootstrap.WithProviderRepository(gitlabArgs.owner, gitlabArgs.repository, gitlabArgs.personal), @@ -258,7 +271,7 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithLogger(logger), bootstrap.WithCABundle(caBundle), - bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyRingPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), + bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) diff --git a/cmd/flux/create_secret_git.go b/cmd/flux/create_secret_git.go index 054c8a98bb..2948a2e076 100644 --- a/cmd/flux/create_secret_git.go +++ b/cmd/flux/create_secret_git.go @@ -21,6 +21,7 @@ import ( "crypto/elliptic" "fmt" "net/url" + "os" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" @@ -135,8 +136,12 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error { } switch u.Scheme { case "ssh": + keypair, err := sourcesecret.LoadKeyPairFromPath(secretGitArgs.privateKeyFile, secretGitArgs.password) + if err != nil { + return err + } + opts.Keypair = keypair opts.SSHHostname = u.Host - opts.PrivateKeyPath = secretGitArgs.privateKeyFile opts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(secretGitArgs.keyAlgorithm) opts.RSAKeyBits = int(secretGitArgs.rsaBits) opts.ECDSACurve = secretGitArgs.ecdsaCurve.Curve @@ -147,7 +152,13 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error { } opts.Username = secretGitArgs.username opts.Password = secretGitArgs.password - opts.CAFilePath = secretGitArgs.caFile + if secretGitArgs.caFile != "" { + caBundle, err := os.ReadFile(secretGitArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + opts.CAFile = caBundle + } default: return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme) } diff --git a/cmd/flux/create_secret_helm.go b/cmd/flux/create_secret_helm.go index 1928c109eb..aba9e734ef 100644 --- a/cmd/flux/create_secret_helm.go +++ b/cmd/flux/create_secret_helm.go @@ -18,6 +18,8 @@ package main import ( "context" + "fmt" + "os" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" @@ -74,15 +76,34 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error { return err } + caBundle := []byte{} + if secretHelmArgs.caFile != "" { + var err error + caBundle, err = os.ReadFile(secretHelmArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + } + + var certFile, keyFile []byte + if secretHelmArgs.certFile != "" && secretHelmArgs.keyFile != "" { + if certFile, err = os.ReadFile(secretHelmArgs.certFile); err != nil { + return fmt.Errorf("failed to read cert file: %w", err) + } + if keyFile, err = os.ReadFile(secretHelmArgs.keyFile); err != nil { + return fmt.Errorf("failed to read key file: %w", err) + } + } + opts := sourcesecret.Options{ - Name: name, - Namespace: *kubeconfigArgs.Namespace, - Labels: labels, - Username: secretHelmArgs.username, - Password: secretHelmArgs.password, - CAFilePath: secretHelmArgs.caFile, - CertFilePath: secretHelmArgs.certFile, - KeyFilePath: secretHelmArgs.keyFile, + Name: name, + Namespace: *kubeconfigArgs.Namespace, + Labels: labels, + Username: secretHelmArgs.username, + Password: secretHelmArgs.password, + CAFile: caBundle, + CertFile: certFile, + KeyFile: keyFile, } secret, err := sourcesecret.Generate(opts) if err != nil { diff --git a/cmd/flux/create_secret_tls.go b/cmd/flux/create_secret_tls.go index e3afd3806b..640fef5d74 100644 --- a/cmd/flux/create_secret_tls.go +++ b/cmd/flux/create_secret_tls.go @@ -18,6 +18,8 @@ package main import ( "context" + "fmt" + "os" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -73,13 +75,32 @@ func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error { return err } + caBundle := []byte{} + if secretTLSArgs.caFile != "" { + var err error + caBundle, err = os.ReadFile(secretTLSArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + } + + var certFile, keyFile []byte + if secretTLSArgs.certFile != "" && secretTLSArgs.keyFile != "" { + if certFile, err = os.ReadFile(secretTLSArgs.certFile); err != nil { + return fmt.Errorf("failed to read cert file: %w", err) + } + if keyFile, err = os.ReadFile(secretTLSArgs.keyFile); err != nil { + return fmt.Errorf("failed to read key file: %w", err) + } + } + opts := sourcesecret.Options{ - Name: name, - Namespace: *kubeconfigArgs.Namespace, - Labels: labels, - CAFilePath: secretTLSArgs.caFile, - CertFilePath: secretTLSArgs.certFile, - KeyFilePath: secretTLSArgs.keyFile, + Name: name, + Namespace: *kubeconfigArgs.Namespace, + Labels: labels, + CAFile: caBundle, + CertFile: certFile, + KeyFile: keyFile, } secret, err := sourcesecret.Generate(opts) if err != nil { diff --git a/cmd/flux/create_source_git.go b/cmd/flux/create_source_git.go index c5cb9e1a09..22306dc5b3 100644 --- a/cmd/flux/create_source_git.go +++ b/cmd/flux/create_source_git.go @@ -259,16 +259,26 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { } switch u.Scheme { case "ssh": + keypair, err := sourcesecret.LoadKeyPairFromPath(sourceGitArgs.privateKeyFile, sourceGitArgs.password) + if err != nil { + return err + } + secretOpts.Keypair = keypair secretOpts.SSHHostname = u.Host - secretOpts.PrivateKeyPath = sourceGitArgs.privateKeyFile secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(sourceGitArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(sourceGitArgs.keyRSABits) secretOpts.ECDSACurve = sourceGitArgs.keyECDSACurve.Curve secretOpts.Password = sourceGitArgs.password case "https": + if sourceGitArgs.caFile != "" { + caBundle, err := os.ReadFile(sourceGitArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + secretOpts.CAFile = caBundle + } secretOpts.Username = sourceGitArgs.username secretOpts.Password = sourceGitArgs.password - secretOpts.CAFilePath = sourceGitArgs.caFile case "http": logger.Warningf("insecure configuration: credentials configured for an HTTP URL") secretOpts.Username = sourceGitArgs.username diff --git a/cmd/flux/create_source_helm.go b/cmd/flux/create_source_helm.go index 4b56f37a4d..49a4b026d4 100644 --- a/cmd/flux/create_source_helm.go +++ b/cmd/flux/create_source_helm.go @@ -168,6 +168,25 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { return err } + caBundle := []byte{} + if sourceHelmArgs.caFile != "" { + var err error + caBundle, err = os.ReadFile(sourceHelmArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + } + + var certFile, keyFile []byte + if sourceHelmArgs.certFile != "" && sourceHelmArgs.keyFile != "" { + if certFile, err = os.ReadFile(sourceHelmArgs.certFile); err != nil { + return fmt.Errorf("failed to read cert file: %w", err) + } + if keyFile, err = os.ReadFile(sourceHelmArgs.keyFile); err != nil { + return fmt.Errorf("failed to read key file: %w", err) + } + } + logger.Generatef("generating HelmRepository source") if sourceHelmArgs.secretRef == "" { secretName := fmt.Sprintf("helm-%s", name) @@ -176,9 +195,9 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Username: sourceHelmArgs.username, Password: sourceHelmArgs.password, - CertFilePath: sourceHelmArgs.certFile, - KeyFilePath: sourceHelmArgs.keyFile, - CAFilePath: sourceHelmArgs.caFile, + CAFile: caBundle, + CertFile: certFile, + KeyFile: keyFile, ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } secret, err := sourcesecret.Generate(secretOpts) diff --git a/pkg/bootstrap/bootstrap_plain_git.go b/pkg/bootstrap/bootstrap_plain_git.go index 909eb3b213..4f82a72e49 100644 --- a/pkg/bootstrap/bootstrap_plain_git.go +++ b/pkg/bootstrap/bootstrap_plain_git.go @@ -24,6 +24,7 @@ import ( "strings" "time" + "github.com/ProtonMail/go-crypto/openpgp" gogit "github.com/go-git/go-git/v5" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -56,9 +57,9 @@ type PlainGitBootstrapper struct { author git.Author commitMessageAppendix string - gpgKeyRingPath string - gpgPassphrase string - gpgKeyID string + gpgKeyRing openpgp.EntityList + gpgPassphrase string + gpgKeyID string restClientGetter genericclioptions.RESTClientGetter restClientOptions *runclient.Options @@ -139,7 +140,7 @@ func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifest } // Git commit generated - gpgOpts := git.WithGpgSigningOption(b.gpgKeyRingPath, b.gpgPassphrase, b.gpgKeyID) + gpgOpts := git.WithGpgSigningOption(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version) if b.commitMessageAppendix != "" { commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix @@ -195,7 +196,7 @@ func (b *PlainGitBootstrapper) ReconcileSourceSecret(ctx context.Context, option } // Return early if exists and no custom config is passed - if ok && len(options.CAFilePath+options.PrivateKeyPath+options.Username+options.Password) == 0 { + if ok && options.Keypair == nil && len(options.CAFile) == 0 && len(options.Username+options.Password) == 0 { b.logger.Successf("source secret up to date") return nil } @@ -284,7 +285,7 @@ func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options b.logger.Successf("generated sync manifests") // Git commit generated - gpgOpts := git.WithGpgSigningOption(b.gpgKeyRingPath, b.gpgPassphrase, b.gpgKeyID) + gpgOpts := git.WithGpgSigningOption(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) commitMsg := fmt.Sprintf("Add Flux sync manifests") if b.commitMessageAppendix != "" { commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix diff --git a/pkg/bootstrap/git/commit_options.go b/pkg/bootstrap/git/commit_options.go index e39614d20d..b30491f6fe 100644 --- a/pkg/bootstrap/git/commit_options.go +++ b/pkg/bootstrap/git/commit_options.go @@ -1,5 +1,9 @@ package git +import ( + "github.com/ProtonMail/go-crypto/openpgp" +) + // Option is a some configuration that modifies options for a commit. type Option interface { // ApplyToCommit applies this configuration to a given commit option. @@ -13,9 +17,9 @@ type CommitOptions struct { // GPGSigningInfo contains information for signing a commit. type GPGSigningInfo struct { - KeyRingPath string - Passphrase string - KeyID string + KeyRing openpgp.EntityList + Passphrase string + KeyID string } type GpgSigningOption struct { @@ -26,17 +30,17 @@ func (w GpgSigningOption) ApplyToCommit(in *CommitOptions) { in.GPGSigningInfo = w.GPGSigningInfo } -func WithGpgSigningOption(path, passphrase, keyID string) Option { +func WithGpgSigningOption(keyRing openpgp.EntityList, passphrase, keyID string) Option { // Return nil if no path is set, even if other options are configured. - if path == "" { + if len(keyRing) == 0 { return GpgSigningOption{} } return GpgSigningOption{ GPGSigningInfo: &GPGSigningInfo{ - KeyRingPath: path, - Passphrase: passphrase, - KeyID: keyID, + KeyRing: keyRing, + Passphrase: passphrase, + KeyID: keyID, }, } } diff --git a/pkg/bootstrap/git/gogit/gogit.go b/pkg/bootstrap/git/gogit/gogit.go index 559633b581..c1c6200a77 100644 --- a/pkg/bootstrap/git/gogit/gogit.go +++ b/pkg/bootstrap/git/gogit/gogit.go @@ -258,23 +258,13 @@ func isRemoteBranchNotFoundErr(err error, ref string) bool { } func getOpenPgpEntity(info git.GPGSigningInfo) (*openpgp.Entity, error) { - r, err := os.Open(info.KeyRingPath) - if err != nil { - return nil, fmt.Errorf("unable to open GPG key ring: %w", err) - } - - entityList, err := openpgp.ReadKeyRing(r) - if err != nil { - return nil, err - } - - if len(entityList) == 0 { + if len(info.KeyRing) == 0 { return nil, fmt.Errorf("empty GPG key ring") } var entity *openpgp.Entity if info.KeyID != "" { - for _, ent := range entityList { + for _, ent := range info.KeyRing { if ent.PrimaryKey.KeyIdString() == info.KeyID { entity = ent } @@ -284,10 +274,10 @@ func getOpenPgpEntity(info git.GPGSigningInfo) (*openpgp.Entity, error) { return nil, fmt.Errorf("no GPG private key matching key id '%s' found", info.KeyID) } } else { - entity = entityList[0] + entity = info.KeyRing[0] } - err = entity.PrivateKey.Decrypt([]byte(info.Passphrase)) + err := entity.PrivateKey.Decrypt([]byte(info.Passphrase)) if err != nil { return nil, fmt.Errorf("unable to decrypt GPG private key: %w", err) } diff --git a/pkg/bootstrap/git/gogit/gogit_test.go b/pkg/bootstrap/git/gogit/gogit_test.go index 02c5ea5879..03e4545b5e 100644 --- a/pkg/bootstrap/git/gogit/gogit_test.go +++ b/pkg/bootstrap/git/gogit/gogit_test.go @@ -4,8 +4,10 @@ package gogit import ( + "os" "testing" + "github.com/ProtonMail/go-crypto/openpgp" "github.com/fluxcd/flux2/pkg/bootstrap/git" ) @@ -49,10 +51,21 @@ func TestGetOpenPgpEntity(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + var entityList openpgp.EntityList + if tt.keyPath != "" { + r, err := os.Open(tt.keyPath) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + entityList, err = openpgp.ReadKeyRing(r) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + } gpgInfo := git.GPGSigningInfo{ - KeyRingPath: tt.keyPath, - Passphrase: tt.passphrase, - KeyID: tt.id, + KeyRing: entityList, + Passphrase: tt.passphrase, + KeyID: tt.id, } _, err := getOpenPgpEntity(gpgInfo) diff --git a/pkg/bootstrap/options.go b/pkg/bootstrap/options.go index 9dada56f93..7ab229b5c6 100644 --- a/pkg/bootstrap/options.go +++ b/pkg/bootstrap/options.go @@ -19,6 +19,7 @@ package bootstrap import ( "k8s.io/cli-runtime/pkg/genericclioptions" + "github.com/ProtonMail/go-crypto/openpgp" runclient "github.com/fluxcd/pkg/runtime/client" "github.com/fluxcd/flux2/pkg/bootstrap/git" @@ -131,22 +132,22 @@ func (o loggerOption) applyGitProvider(b *GitProviderBootstrapper) { b.logger = o.logger } -func WithGitCommitSigning(path, passphrase, keyID string) Option { +func WithGitCommitSigning(gpgKeyRing openpgp.EntityList, passphrase, keyID string) Option { return gitCommitSigningOption{ - gpgKeyRingPath: path, - gpgPassphrase: passphrase, - gpgKeyID: keyID, + gpgKeyRing: gpgKeyRing, + gpgPassphrase: passphrase, + gpgKeyID: keyID, } } type gitCommitSigningOption struct { - gpgKeyRingPath string - gpgPassphrase string - gpgKeyID string + gpgKeyRing openpgp.EntityList + gpgPassphrase string + gpgKeyID string } func (o gitCommitSigningOption) applyGit(b *PlainGitBootstrapper) { - b.gpgKeyRingPath = o.gpgKeyRingPath + b.gpgKeyRing = o.gpgKeyRing b.gpgPassphrase = o.gpgPassphrase b.gpgKeyID = o.gpgKeyID } diff --git a/pkg/manifestgen/sourcesecret/options.go b/pkg/manifestgen/sourcesecret/options.go index 371e6f9b15..d9cec044b7 100644 --- a/pkg/manifestgen/sourcesecret/options.go +++ b/pkg/manifestgen/sourcesecret/options.go @@ -18,6 +18,8 @@ package sourcesecret import ( "crypto/elliptic" + + "github.com/fluxcd/pkg/ssh" ) type PrivateKeyAlgorithm string @@ -48,12 +50,12 @@ type Options struct { PrivateKeyAlgorithm PrivateKeyAlgorithm RSAKeyBits int ECDSACurve elliptic.Curve - PrivateKeyPath string + Keypair *ssh.KeyPair Username string Password string - CAFilePath string - CertFilePath string - KeyFilePath string + CAFile []byte + CertFile []byte + KeyFile []byte TargetPath string ManifestFile string } @@ -64,12 +66,11 @@ func MakeDefaultOptions() Options { Namespace: "flux-system", Labels: map[string]string{}, PrivateKeyAlgorithm: RSAPrivateKeyAlgorithm, - PrivateKeyPath: "", Username: "", Password: "", - CAFilePath: "", - CertFilePath: "", - KeyFilePath: "", + CAFile: []byte{}, + CertFile: []byte{}, + KeyFile: []byte{}, ManifestFile: "secret.yaml", } } diff --git a/pkg/manifestgen/sourcesecret/sourcesecret.go b/pkg/manifestgen/sourcesecret/sourcesecret.go index 87567168d0..84d02cbe06 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret.go @@ -65,11 +65,9 @@ func Generate(options Options) (*manifestgen.Manifest, error) { var keypair *ssh.KeyPair switch { case options.Username != "" && options.Password != "": - // noop - case len(options.PrivateKeyPath) > 0: - if keypair, err = loadKeyPair(options.PrivateKeyPath, options.Password); err != nil { - return nil, err - } + // noop + case options.Keypair != nil: + keypair = options.Keypair case len(options.PrivateKeyAlgorithm) > 0: if keypair, err = generateKeyPair(options); err != nil { return nil, err @@ -83,23 +81,6 @@ func Generate(options Options) (*manifestgen.Manifest, error) { } } - var caFile []byte - if options.CAFilePath != "" { - if caFile, err = os.ReadFile(options.CAFilePath); err != nil { - return nil, fmt.Errorf("failed to read CA file: %w", err) - } - } - - var certFile, keyFile []byte - if options.CertFilePath != "" && options.KeyFilePath != "" { - if certFile, err = os.ReadFile(options.CertFilePath); err != nil { - return nil, fmt.Errorf("failed to read cert file: %w", err) - } - if keyFile, err = os.ReadFile(options.KeyFilePath); err != nil { - return nil, fmt.Errorf("failed to read key file: %w", err) - } - } - var dockerCfgJson []byte if options.Registry != "" { dockerCfgJson, err = generateDockerConfigJson(options.Registry, options.Username, options.Password) @@ -108,7 +89,7 @@ func Generate(options Options) (*manifestgen.Manifest, error) { } } - secret := buildSecret(keypair, hostKey, caFile, certFile, keyFile, dockerCfgJson, options) + secret := buildSecret(keypair, hostKey, options.CAFile, options.CertFile, options.KeyFile, dockerCfgJson, options) b, err := yaml.Marshal(secret) if err != nil { return nil, err @@ -120,6 +101,35 @@ func Generate(options Options) (*manifestgen.Manifest, error) { }, nil } +func LoadKeyPairFromPath(path, password string) (*ssh.KeyPair, error) { + if path == "" { + return nil, nil + } + + b, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to open private key file: %w", err) + } + return LoadKeyPair(b, password) +} + +func LoadKeyPair(privateKey []byte, password string) (*ssh.KeyPair, error) { + var ppk cryptssh.Signer + var err error + if password != "" { + ppk, err = cryptssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(password)) + } else { + ppk, err = cryptssh.ParsePrivateKey(privateKey) + } + if err != nil { + return nil, err + } + return &ssh.KeyPair{ + PublicKey: cryptssh.MarshalAuthorizedKey(ppk.PublicKey()), + PrivateKey: privateKey, + }, nil +} + func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile, dockerCfg []byte, options Options) (secret corev1.Secret) { secret.TypeMeta = metav1.TypeMeta{ APIVersion: "v1", @@ -143,16 +153,16 @@ func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile, docke secret.StringData[PasswordSecretKey] = options.Password } - if caFile != nil { + if len(caFile) != 0 { secret.StringData[CAFileSecretKey] = string(caFile) } - if certFile != nil && keyFile != nil { + if len(certFile) != 0 && len(keyFile) != 0 { secret.StringData[CertFileSecretKey] = string(certFile) secret.StringData[KeyFileSecretKey] = string(keyFile) } - if keypair != nil && hostKey != nil { + if keypair != nil && len(hostKey) != 0 { secret.StringData[PrivateKeySecretKey] = string(keypair.PrivateKey) secret.StringData[PublicKeySecretKey] = string(keypair.PublicKey) secret.StringData[KnownHostsSecretKey] = string(hostKey) @@ -165,29 +175,6 @@ func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile, docke return } -func loadKeyPair(path string, password string) (*ssh.KeyPair, error) { - b, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("failed to open private key file: %w", err) - } - - var ppk cryptssh.Signer - if password != "" { - ppk, err = cryptssh.ParsePrivateKeyWithPassphrase(b, []byte(password)) - } else { - ppk, err = cryptssh.ParsePrivateKey(b) - } - - if err != nil { - return nil, err - } - - return &ssh.KeyPair{ - PublicKey: cryptssh.MarshalAuthorizedKey(ppk.PublicKey()), - PrivateKey: b, - }, nil -} - func generateKeyPair(options Options) (*ssh.KeyPair, error) { var keyGen ssh.KeyPairGenerator switch options.PrivateKeyAlgorithm { diff --git a/pkg/manifestgen/sourcesecret/sourcesecret_test.go b/pkg/manifestgen/sourcesecret/sourcesecret_test.go index e225bbd8ea..8eb619d451 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret_test.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret_test.go @@ -48,7 +48,7 @@ func Test_passwordLoadKeyPair(t *testing.T) { pk, _ := os.ReadFile(tt.privateKeyPath) ppk, _ := os.ReadFile(tt.publicKeyPath) - got, err := loadKeyPair(tt.privateKeyPath, tt.password) + got, err := LoadKeyPair(pk, tt.password) if err != nil { t.Errorf("loadKeyPair() error = %v", err) return @@ -67,24 +67,13 @@ func Test_passwordLoadKeyPair(t *testing.T) { func Test_PasswordlessLoadKeyPair(t *testing.T) { for algo, privateKey := range testdata.PEMBytes { t.Run(algo, func(t *testing.T) { - f, err := os.CreateTemp("", "test-private-key-") - if err != nil { - t.Fatalf("unable to create temporary file. err: %s", err) - } - defer os.Remove(f.Name()) - - if _, err = f.Write(privateKey); err != nil { - t.Fatalf("unable to write private key to file. err: %s", err) - } - - got, err := loadKeyPair(f.Name(), "") + got, err := LoadKeyPair(privateKey, "") if err != nil { t.Errorf("loadKeyPair() error = %v", err) return } - pk, _ := os.ReadFile(f.Name()) - if !reflect.DeepEqual(got.PrivateKey, pk) { + if !reflect.DeepEqual(got.PrivateKey, privateKey) { t.Errorf("PrivateKey %s != %s", got.PrivateKey, string(privateKey)) }