From acd385f91c7c793596babeeede57fb23452d5416 Mon Sep 17 00:00:00 2001 From: Marko Mikulicic Date: Tue, 6 Aug 2019 16:59:26 +0200 Subject: [PATCH] Handle items sealed with different keys Ref #185 This is not the best implementation, but it technically unblocks #185 and lets us incrementally implement a better solution (the current plan is to make kubeseal encode the fingerprint of the public key used to seal the secret in each item). --- cmd/controller/controller.go | 11 ++- integration/controller_test.go | 7 +- integration/kubeseal_test.go | 12 ++-- .../v1alpha1/sealedsecret_expansion.go | 8 +-- .../v1alpha1/sealedsecret_test.go | 68 +++++++++---------- pkg/crypto/crypto.go | 17 ++++- 6 files changed, 66 insertions(+), 57 deletions(-) diff --git a/cmd/controller/controller.go b/cmd/controller/controller.go index 2420d7a01..f11c146e6 100644 --- a/cmd/controller/controller.go +++ b/cmd/controller/controller.go @@ -1,6 +1,7 @@ package main import ( + "crypto/rsa" "encoding/json" "fmt" "log" @@ -360,11 +361,9 @@ func (c *Controller) attemptUnseal(ss *ssv1alpha1.SealedSecret) (*corev1.Secret, } func attemptUnseal(ss *ssv1alpha1.SealedSecret, keyRegistry *KeyRegistry) (*corev1.Secret, error) { - // TODO(mkm): embed the pubkey fingerprint in each sealed item so we can fetch the right key - for _, key := range keyRegistry.keys { - if secret, err := ss.Unseal(scheme.Codecs, key.private); err == nil { - return secret, nil - } + privateKeys := map[string]*rsa.PrivateKey{} + for k, v := range keyRegistry.keys { + privateKeys[k] = v.private } - return nil, fmt.Errorf("No key could decrypt secret") + return ss.Unseal(scheme.Codecs, privateKeys) } diff --git a/integration/controller_test.go b/integration/controller_test.go index 33b6bc30d..352470de9 100644 --- a/integration/controller_test.go +++ b/integration/controller_test.go @@ -47,7 +47,7 @@ func getSecretType(s *v1.Secret) v1.SecretType { return s.Type } -func fetchKeys(c corev1.SecretsGetter) (*rsa.PrivateKey, []*x509.Certificate, error) { +func fetchKeys(c corev1.SecretsGetter) (map[string]*rsa.PrivateKey, []*x509.Certificate, error) { list, err := c.Secrets("kube-system").List(metav1.ListOptions{ LabelSelector: keySelector, }) @@ -76,7 +76,10 @@ func fetchKeys(c corev1.SecretsGetter) (*rsa.PrivateKey, []*x509.Certificate, er return nil, nil, fmt.Errorf("Failed to read any certificates") } - return privKey.(*rsa.PrivateKey), certs, nil + rsaPrivKey := privKey.(*rsa.PrivateKey) + fp, err := crypto.PublicKeyFingerprint(&rsaPrivKey.PublicKey) + privKeys := map[string]*rsa.PrivateKey{fp: rsaPrivKey} + return privKeys, certs, nil } func containEventWithReason(matcher types.GomegaMatcher) types.GomegaMatcher { diff --git a/integration/kubeseal_test.go b/integration/kubeseal_test.go index e6c992c66..8bab732d6 100644 --- a/integration/kubeseal_test.go +++ b/integration/kubeseal_test.go @@ -32,7 +32,7 @@ var _ = Describe("kubeseal", func() { var input *v1.Secret var ss *ssv1alpha1.SealedSecret var args []string - var privKey *rsa.PrivateKey + var privKeys map[string]*rsa.PrivateKey var certs []*x509.Certificate var config *clientcmdapi.Config var kubeconfigFile string @@ -80,7 +80,7 @@ var _ = Describe("kubeseal", func() { } var err error - privKey, certs, err = fetchKeys(c) + privKeys, certs, err = fetchKeys(c) Expect(err).NotTo(HaveOccurred()) }) @@ -103,7 +103,7 @@ var _ = Describe("kubeseal", func() { }) It("should contain the right value", func() { - s, err := ss.Unseal(scheme.Codecs, privKey) + s, err := ss.Unseal(scheme.Codecs, privKeys) Expect(err).NotTo(HaveOccurred()) Expect(s.Data).To(HaveKeyWithValue("foo", []byte("bar"))) }) @@ -122,7 +122,7 @@ var _ = Describe("kubeseal", func() { }) It("should qualify the Secret", func() { - s, err := ss.Unseal(scheme.Codecs, privKey) + s, err := ss.Unseal(scheme.Codecs, privKeys) Expect(err).NotTo(HaveOccurred()) Expect(s.GetNamespace()).To(Equal(testNs)) }) @@ -139,7 +139,7 @@ var _ = Describe("kubeseal", func() { }) It("should qualify the Secret", func() { - s, err := ss.Unseal(scheme.Codecs, privKey) + s, err := ss.Unseal(scheme.Codecs, privKeys) Expect(err).NotTo(HaveOccurred()) Expect(s.GetNamespace()).To(Equal(testNs)) }) @@ -174,7 +174,7 @@ var _ = Describe("kubeseal", func() { }) It("should output the right value", func() { - s, err := ss.Unseal(scheme.Codecs, privKey) + s, err := ss.Unseal(scheme.Codecs, privKeys) Expect(err).NotTo(HaveOccurred()) Expect(s.Data).To(HaveKeyWithValue("foo", []byte("bar"))) }) diff --git a/pkg/apis/sealed-secrets/v1alpha1/sealedsecret_expansion.go b/pkg/apis/sealed-secrets/v1alpha1/sealedsecret_expansion.go index 5e4816020..3e2414d58 100644 --- a/pkg/apis/sealed-secrets/v1alpha1/sealedsecret_expansion.go +++ b/pkg/apis/sealed-secrets/v1alpha1/sealedsecret_expansion.go @@ -16,7 +16,7 @@ import ( // SealedSecretExpansion has methods to work with SealedSecrets resources. type SealedSecretExpansion interface { - Unseal(codecs runtimeserializer.CodecFactory, privKey *rsa.PrivateKey) (*v1.Secret, error) + Unseal(codecs runtimeserializer.CodecFactory, privKeys map[string]*rsa.PrivateKey) (*v1.Secret, error) } // Returns labels followed by clusterWide followed by namespaceWide. @@ -136,7 +136,7 @@ func NewSealedSecret(codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKe } // Unseal decrypts and returns the embedded v1.Secret. -func (s *SealedSecret) Unseal(codecs runtimeserializer.CodecFactory, privKey *rsa.PrivateKey) (*v1.Secret, error) { +func (s *SealedSecret) Unseal(codecs runtimeserializer.CodecFactory, privKeys map[string]*rsa.PrivateKey) (*v1.Secret, error) { boolTrue := true smeta := s.GetObjectMeta() @@ -157,7 +157,7 @@ func (s *SealedSecret) Unseal(codecs runtimeserializer.CodecFactory, privKey *rs if err != nil { return nil, err } - plaintext, err := crypto.HybridDecrypt(rand.Reader, privKey, valueBytes, label) + plaintext, err := crypto.HybridDecrypt(rand.Reader, privKeys, valueBytes, label) if err != nil { return nil, err } @@ -165,7 +165,7 @@ func (s *SealedSecret) Unseal(codecs runtimeserializer.CodecFactory, privKey *rs } } else { // Support decrypting old secrets for backward compatibility - plaintext, err := crypto.HybridDecrypt(rand.Reader, privKey, s.Spec.Data, label) + plaintext, err := crypto.HybridDecrypt(rand.Reader, privKeys, s.Spec.Data, label) if err != nil { return nil, err } diff --git a/pkg/apis/sealed-secrets/v1alpha1/sealedsecret_test.go b/pkg/apis/sealed-secrets/v1alpha1/sealedsecret_test.go index c8097b4ed..1c46404c3 100644 --- a/pkg/apis/sealed-secrets/v1alpha1/sealedsecret_test.go +++ b/pkg/apis/sealed-secrets/v1alpha1/sealedsecret_test.go @@ -11,6 +11,7 @@ import ( fuzz "github.com/google/gofuzz" + "github.com/bitnami-labs/sealed-secrets/pkg/crypto" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" rttesting "k8s.io/apimachinery/pkg/api/apitesting/roundtrip" @@ -170,6 +171,19 @@ func testRand() io.Reader { return mathrand.New(mathrand.NewSource(42)) } +func generateTestKey(t *testing.T, rand io.Reader, bits int) (*rsa.PrivateKey, map[string]*rsa.PrivateKey) { + key, err := rsa.GenerateKey(rand, 2048) + if err != nil { + t.Fatalf("Failed to generate test key: %v", err) + } + fingerprint, err := crypto.PublicKeyFingerprint(&key.PublicKey) + if err != nil { + t.Fatalf("Failed to generate fingerprint: %v", err) + } + keys := map[string]*rsa.PrivateKey{fingerprint: key} + return key, keys +} + func TestSealRoundTrip(t *testing.T) { scheme := runtime.NewScheme() codecs := serializer.NewCodecFactory(scheme) @@ -177,11 +191,7 @@ func TestSealRoundTrip(t *testing.T) { SchemeBuilder.AddToScheme(scheme) v1.SchemeBuilder.AddToScheme(scheme) - rand := testRand() - key, err := rsa.GenerateKey(rand, 2048) - if err != nil { - t.Fatalf("Failed to generate test key: %v", err) - } + key, keys := generateTestKey(t, testRand(), 2048) secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -198,7 +208,7 @@ func TestSealRoundTrip(t *testing.T) { t.Fatalf("NewSealedSecret returned error: %v", err) } - secret2, err := ssecret.Unseal(codecs, key) + secret2, err := ssecret.Unseal(codecs, keys) if err != nil { t.Fatalf("Unseal returned error: %v", err) } @@ -215,11 +225,7 @@ func TestSealRoundTripStringDataConversion(t *testing.T) { SchemeBuilder.AddToScheme(scheme) v1.SchemeBuilder.AddToScheme(scheme) - rand := testRand() - key, err := rsa.GenerateKey(rand, 2048) - if err != nil { - t.Fatalf("Failed to generate test key: %v", err) - } + key, keys := generateTestKey(t, testRand(), 2048) secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -251,7 +257,7 @@ func TestSealRoundTripStringDataConversion(t *testing.T) { t.Fatalf("NewSealedSecret returned error: %v", err) } - secret2, err := ssecret.Unseal(codecs, key) + secret2, err := ssecret.Unseal(codecs, keys) if err != nil { t.Fatalf("Unseal returned error: %v", err) } @@ -268,11 +274,7 @@ func TestSealRoundTripWithClusterWide(t *testing.T) { SchemeBuilder.AddToScheme(scheme) v1.SchemeBuilder.AddToScheme(scheme) - rand := testRand() - key, err := rsa.GenerateKey(rand, 2048) - if err != nil { - t.Fatalf("Failed to generate test key: %v", err) - } + key, keys := generateTestKey(t, testRand(), 2048) secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -292,7 +294,7 @@ func TestSealRoundTripWithClusterWide(t *testing.T) { t.Fatalf("NewSealedSecret returned error: %v", err) } - secret2, err := ssecret.Unseal(codecs, key) + secret2, err := ssecret.Unseal(codecs, keys) if err != nil { t.Fatalf("Unseal returned error: %v", err) } @@ -309,11 +311,7 @@ func TestSealRoundTripWithMisMatchClusterWide(t *testing.T) { SchemeBuilder.AddToScheme(scheme) v1.SchemeBuilder.AddToScheme(scheme) - rand := testRand() - key, err := rsa.GenerateKey(rand, 2048) - if err != nil { - t.Fatalf("Failed to generate test key: %v", err) - } + key, keys := generateTestKey(t, testRand(), 2048) secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -335,7 +333,7 @@ func TestSealRoundTripWithMisMatchClusterWide(t *testing.T) { ssecret.ObjectMeta.Annotations[SealedSecretClusterWideAnnotation] = "false" - _, err = ssecret.Unseal(codecs, key) + _, err = ssecret.Unseal(codecs, keys) if err == nil { t.Fatalf("Unseal did not return expected error: %v", err) } @@ -348,11 +346,7 @@ func TestSealRoundTripWithNamespaceWide(t *testing.T) { SchemeBuilder.AddToScheme(scheme) v1.SchemeBuilder.AddToScheme(scheme) - rand := testRand() - key, err := rsa.GenerateKey(rand, 2048) - if err != nil { - t.Fatalf("Failed to generate test key: %v", err) - } + key, keys := generateTestKey(t, testRand(), 2048) secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -372,7 +366,7 @@ func TestSealRoundTripWithNamespaceWide(t *testing.T) { t.Fatalf("NewSealedSecret returned error: %v", err) } - secret2, err := ssecret.Unseal(codecs, key) + secret2, err := ssecret.Unseal(codecs, keys) if err != nil { t.Fatalf("Unseal returned error: %v", err) } @@ -389,11 +383,7 @@ func TestSealRoundTripWithMisMatchNamespaceWide(t *testing.T) { SchemeBuilder.AddToScheme(scheme) v1.SchemeBuilder.AddToScheme(scheme) - rand := testRand() - key, err := rsa.GenerateKey(rand, 2048) - if err != nil { - t.Fatalf("Failed to generate test key: %v", err) - } + key, keys := generateTestKey(t, testRand(), 2048) secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -415,7 +405,7 @@ func TestSealRoundTripWithMisMatchNamespaceWide(t *testing.T) { ssecret.ObjectMeta.Annotations[SealedSecretNamespaceWideAnnotation] = "false" - _, err = ssecret.Unseal(codecs, key) + _, err = ssecret.Unseal(codecs, keys) if err == nil { t.Fatalf("Unseal did not return expected error: %v", err) } @@ -453,7 +443,11 @@ func TestUnsealingV1Format(t *testing.T) { t.Fatalf("NewSealedSecret returned error: %v", err) } - secret2, err := ssecret.Unseal(codecs, key) + fp, err := crypto.PublicKeyFingerprint(&key.PublicKey) + if err != nil { + t.Fatalf("cannot compute fingerprint: %v", err) + } + secret2, err := ssecret.Unseal(codecs, map[string]*rsa.PrivateKey{fp: key}) if err != nil { t.Fatalf("Unseal returned error: %v", err) } diff --git a/pkg/crypto/crypto.go b/pkg/crypto/crypto.go index c026f1be2..184ba8b4c 100644 --- a/pkg/crypto/crypto.go +++ b/pkg/crypto/crypto.go @@ -7,6 +7,7 @@ import ( "crypto/sha256" "encoding/binary" "errors" + "fmt" "io" "golang.org/x/crypto/ssh" @@ -69,8 +70,20 @@ func HybridEncrypt(rnd io.Reader, pubKey *rsa.PublicKey, plaintext, label []byte return ciphertext, nil } -// HybridDecrypt performs a regular AES-GCM + RSA-OAEP decryption -func HybridDecrypt(rnd io.Reader, privKey *rsa.PrivateKey, ciphertext, label []byte) ([]byte, error) { +// HybridDecrypt performs a regular AES-GCM + RSA-OAEP decryption. +// The private keys map has a fingerprint of each public key as the map key. +func HybridDecrypt(rnd io.Reader, privKeys map[string]*rsa.PrivateKey, ciphertext, label []byte) ([]byte, error) { + // TODO(mkm): use the key fingerprint encoded in ciphertext (if present) instead of trying all the possible keys + for _, privKey := range privKeys { + if secret, err := singleDecrypt(rnd, privKey, ciphertext, label); err == nil { + return secret, nil + } + } + return nil, fmt.Errorf("no key could decrypt secret") +} + +// singleDecrypt performs a regular AES-GCM + RSA-OAEP decryption +func singleDecrypt(rnd io.Reader, privKey *rsa.PrivateKey, ciphertext, label []byte) ([]byte, error) { if len(ciphertext) < 2 { return nil, ErrTooShort }