Skip to content

Commit

Permalink
Merge #216
Browse files Browse the repository at this point in the history
216: Handle items sealed with different keys r=mkmik a=mkmik

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).

Co-authored-by: Marko Mikulicic <mkm@bitnami.com>
  • Loading branch information
bors[bot] and Marko Mikulicic committed Aug 28, 2019
2 parents 71ab1f4 + acd385f commit 4c6ea3b
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 57 deletions.
11 changes: 5 additions & 6 deletions cmd/controller/controller.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"crypto/rsa"
"encoding/json"
"fmt"
"log"
Expand Down Expand Up @@ -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)
}
7 changes: 5 additions & 2 deletions integration/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 6 additions & 6 deletions integration/kubeseal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
})

Expand All @@ -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")))
})
Expand All @@ -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))
})
Expand All @@ -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))
})
Expand Down Expand Up @@ -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")))
})
Expand Down
8 changes: 4 additions & 4 deletions pkg/apis/sealed-secrets/v1alpha1/sealedsecret_expansion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()

Expand All @@ -157,15 +157,15 @@ 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
}
secret.Data[key] = plaintext
}

} 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
}
Expand Down
68 changes: 31 additions & 37 deletions pkg/apis/sealed-secrets/v1alpha1/sealedsecret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -170,18 +171,27 @@ 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)

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{
Expand All @@ -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)
}
Expand All @@ -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{
Expand Down Expand Up @@ -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)
}
Expand All @@ -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{
Expand All @@ -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)
}
Expand All @@ -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{
Expand All @@ -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)
}
Expand All @@ -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{
Expand All @@ -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)
}
Expand All @@ -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{
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
17 changes: 15 additions & 2 deletions pkg/crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"

"golang.org/x/crypto/ssh"
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit 4c6ea3b

Please sign in to comment.