From cf455f2e345fa6ff6ea8957692a6dcda3e236324 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Tue, 2 May 2023 17:04:13 +0530 Subject: [PATCH] add support for commit sigining PGP key passphrases Add support for decrypting the private key of the commit signing PGP key. The secret specified in `spec.commit.signingKey.secretRef` can now optionally have a `passphrase` key where it's value is the password to be used for decryptin the private key. Signed-off-by: Sanskar Jaiswal --- docs/spec/v1beta1/imageupdateautomations.md | 16 ++++++++++- go.mod | 2 +- .../imageupdateautomation_controller.go | 28 +++++++++++++------ internal/controllers/update_test.go | 15 +++++++--- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/docs/spec/v1beta1/imageupdateautomations.md b/docs/spec/v1beta1/imageupdateautomations.md index 5ae00519..f99000fe 100644 --- a/docs/spec/v1beta1/imageupdateautomations.md +++ b/docs/spec/v1beta1/imageupdateautomations.md @@ -226,7 +226,21 @@ will result in commits with the author `Fluxbot `. The optional `signingKey` field can be used to provide a key to sign commits with. It holds a reference to a secret, which is expected to have a file called `git.asc` containing an -ASCII-armoured PGP key. +ASCII-armoured PGP key. If the private key is protected by a password, you can specify the same +in the secret using the `passphrase` key. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: signing-key + namespace: default +stringData: + git.asc: | + + passphrase: +``` The `messageTemplate` field is a string which will be used as a template for the commit message. If empty, there is a default message; but you will likely want to provide your own, especially if you diff --git a/go.mod b/go.mod index 73f15b02..0e18dd76 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,6 @@ require ( github.com/onsi/gomega v1.27.6 github.com/otiai10/copy v1.9.0 github.com/spf13/pflag v1.0.5 - golang.org/x/crypto v0.7.0 k8s.io/api v0.26.3 k8s.io/apimachinery v0.26.3 k8s.io/client-go v0.26.3 @@ -120,6 +119,7 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.7.0 // indirect golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.6.0 // indirect diff --git a/internal/controllers/imageupdateautomation_controller.go b/internal/controllers/imageupdateautomation_controller.go index 054a5f6c..25c9bc4e 100644 --- a/internal/controllers/imageupdateautomation_controller.go +++ b/internal/controllers/imageupdateautomation_controller.go @@ -67,13 +67,13 @@ import ( "github.com/fluxcd/image-automation-controller/pkg/update" ) -const originRemote = "origin" - -const defaultMessageTemplate = `Update from image update automation` - -const repoRefKey = ".spec.gitRepository" - -const signingSecretKey = "git.asc" +const ( + originRemote = "origin" + defaultMessageTemplate = `Update from image update automation` + repoRefKey = ".spec.gitRepository" + signingSecretKey = "git.asc" + signingPassphraseKey = "passphrase" +) // TemplateData is the type of the value given to the commit message // template. @@ -590,7 +590,19 @@ func (r *ImageUpdateAutomationReconciler) getSigningEntity(ctx context.Context, if len(entities) > 1 { return nil, fmt.Errorf("multiple entities read from secret '%s', could not determine which signing key to use", secretName) } - return entities[0], nil + + entity := entities[0] + if entity.PrivateKey.Encrypted { + passphrase, ok := secret.Data[signingPassphraseKey] + if !ok { + return nil, fmt.Errorf("can not use passphrase protected signing key without '%s' field present in secret %s", + signingPassphraseKey, secretName) + } + if err = entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil { + return nil, fmt.Errorf("could not decrypt private key of the signing key present in secret %s: %w", secretName, err) + } + } + return entity, nil } // --- events, metrics diff --git a/internal/controllers/update_test.go b/internal/controllers/update_test.go index 137db079..b40387e9 100644 --- a/internal/controllers/update_test.go +++ b/internal/controllers/update_test.go @@ -31,12 +31,12 @@ import ( "testing" "time" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-logr/logr" . "github.com/onsi/gomega" "github.com/otiai10/copy" - "golang.org/x/crypto/openpgp" - "golang.org/x/crypto/openpgp/armor" corev1 "k8s.io/api/core/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -417,7 +417,7 @@ func TestImageAutomationReconciler_signedCommit(t *testing.T) { kr := openpgp.EntityList([]*openpgp.Entity{pgpEntity}) signature := strings.NewReader(commit.PGPSignature) - _, err = openpgp.CheckArmoredDetachedSignature(kr, content, signature) + _, err = openpgp.CheckArmoredDetachedSignature(kr, content, signature, nil) g.Expect(err).ToNot(HaveOccurred()) }, ) @@ -1581,6 +1581,7 @@ func createSigningKeyPair(kClient client.Client, name, namespace string) (*openp if err != nil { return nil, err } + // Configure OpenPGP armor encoder. b := bytes.NewBuffer(nil) w, err := armor.Encode(b, openpgp.PrivateKeyType, nil) @@ -1594,10 +1595,16 @@ func createSigningKeyPair(kClient client.Client, name, namespace string) (*openp if err = w.Close(); err != nil { return nil, err } + + passphrase := "abcde12345" + if err = pgpEntity.PrivateKey.Encrypt([]byte(passphrase)); err != nil { + return nil, err + } // Create the secret containing signing key. sec := &corev1.Secret{ Data: map[string][]byte{ - "git.asc": b.Bytes(), + signingSecretKey: b.Bytes(), + signingPassphraseKey: []byte(passphrase), }, } sec.Name = name