diff --git a/internal/sops/azkv/keysource.go b/internal/sops/azkv/keysource.go index 58188273e..5ff2be1b7 100644 --- a/internal/sops/azkv/keysource.go +++ b/internal/sops/azkv/keysource.go @@ -118,7 +118,7 @@ func (key *MasterKey) Decrypt() ([]byte, error) { // with the latest. rawEncryptedKey, err := base64.RawURLEncoding.DecodeString(key.EncryptedKey) if err != nil { - return nil, fmt.Errorf("failed to decode encrypted key: %w", err) + return nil, fmt.Errorf("failed to base64 decode Azure Key Vault encrypted key: %w", err) } resp, err := c.Decrypt(context.Background(), crypto.EncryptionAlgorithmRSAOAEP256, rawEncryptedKey, nil) if err != nil { diff --git a/internal/sops/keyservice/server.go b/internal/sops/keyservice/server.go index 3b726eb1e..545608e44 100644 --- a/internal/sops/keyservice/server.go +++ b/internal/sops/keyservice/server.go @@ -5,10 +5,10 @@ package keyservice import ( + "fmt" + "go.mozilla.org/sops/v3/keyservice" "golang.org/x/net/context" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "github.com/fluxcd/kustomize-controller/internal/sops/age" "github.com/fluxcd/kustomize-controller/internal/sops/azkv" @@ -29,7 +29,7 @@ type Server struct { // keyring. gnuPGHome pgp.GnuPGHome - // ageIdentities holds the parsed age identities used for Decrypt + // ageIdentities are the parsed age identities used for Decrypt // operations for age key types. ageIdentities age.ParsedIdentities @@ -86,6 +86,16 @@ func (ks Server) Encrypt(ctx context.Context, req *keyservice.EncryptRequest) (* return &keyservice.EncryptResponse{ Ciphertext: ciphertext, }, nil + case *keyservice.Key_VaultKey: + if ks.vaultToken != "" { + ciphertext, err := ks.encryptWithHCVault(k.VaultKey, req.Plaintext) + if err != nil { + return nil, err + } + return &keyservice.EncryptResponse{ + Ciphertext: ciphertext, + }, nil + } case *keyservice.Key_AzureKeyvaultKey: if ks.azureToken != nil { ciphertext, err := ks.encryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Plaintext) @@ -97,7 +107,7 @@ func (ks Server) Encrypt(ctx context.Context, req *keyservice.EncryptRequest) (* }, nil } case nil: - return nil, status.Errorf(codes.NotFound, "must provide a key") + return nil, fmt.Errorf("must provide a key") } // Fallback to default server for any other request. return ks.defaultServer.Encrypt(ctx, req) @@ -126,7 +136,7 @@ func (ks Server) Decrypt(ctx context.Context, req *keyservice.DecryptRequest) (* }, nil case *keyservice.Key_VaultKey: if ks.vaultToken != "" { - plaintext, err := ks.decryptWithVault(k.VaultKey, req.Ciphertext) + plaintext, err := ks.decryptWithHCVault(k.VaultKey, req.Ciphertext) if err != nil { return nil, err } @@ -145,7 +155,7 @@ func (ks Server) Decrypt(ctx context.Context, req *keyservice.DecryptRequest) (* }, nil } case nil: - return nil, status.Errorf(codes.NotFound, "must provide a key") + return nil, fmt.Errorf("must provide a key") } // Fallback to default server for any other request. return ks.defaultServer.Decrypt(ctx, req) @@ -197,11 +207,20 @@ func (ks *Server) decryptWithAge(key *keyservice.AgeKey, ciphertext []byte) ([]b return plaintext, err } -func (ks *Server) decryptWithVault(key *keyservice.VaultKey, ciphertext []byte) ([]byte, error) { - if ks.vaultToken == "" { - return nil, status.Errorf(codes.Unimplemented, "Hashicorp Vault decrypt service unavailable: no token found") +func (ks *Server) encryptWithHCVault(key *keyservice.VaultKey, plaintext []byte) ([]byte, error) { + vaultKey := hcvault.MasterKey{ + VaultAddress: key.VaultAddress, + EnginePath: key.EnginePath, + KeyName: key.KeyName, + } + ks.vaultToken.ApplyToMasterKey(&vaultKey) + if err := vaultKey.Encrypt(plaintext); err != nil { + return nil, err } + return []byte(vaultKey.EncryptedKey), nil +} +func (ks *Server) decryptWithHCVault(key *keyservice.VaultKey, ciphertext []byte) ([]byte, error) { vaultKey := hcvault.MasterKey{ VaultAddress: key.VaultAddress, EnginePath: key.EnginePath, @@ -214,10 +233,6 @@ func (ks *Server) decryptWithVault(key *keyservice.VaultKey, ciphertext []byte) } func (ks *Server) encryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, plaintext []byte) ([]byte, error) { - if ks.azureToken == nil { - return nil, status.Errorf(codes.Unimplemented, "Azure Key Vault encrypt service unavailable: no authentication config present") - } - azureKey := azkv.MasterKey{ VaultURL: key.VaultUrl, Name: key.Name, @@ -231,10 +246,6 @@ func (ks *Server) encryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, pla } func (ks *Server) decryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, ciphertext []byte) ([]byte, error) { - if ks.azureToken == nil { - return nil, status.Errorf(codes.Unimplemented, "Azure Key Vault decrypt service unavailable: no authentication config present") - } - azureKey := azkv.MasterKey{ VaultURL: key.VaultUrl, Name: key.Name, diff --git a/internal/sops/keyservice/server_test.go b/internal/sops/keyservice/server_test.go new file mode 100644 index 000000000..2231a930b --- /dev/null +++ b/internal/sops/keyservice/server_test.go @@ -0,0 +1,214 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package keyservice + +import ( + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + . "github.com/onsi/gomega" + "go.mozilla.org/sops/v3/keyservice" + "golang.org/x/net/context" + + "github.com/fluxcd/kustomize-controller/internal/sops/age" + "github.com/fluxcd/kustomize-controller/internal/sops/azkv" + "github.com/fluxcd/kustomize-controller/internal/sops/hcvault" + "github.com/fluxcd/kustomize-controller/internal/sops/pgp" +) + +func TestServer_EncryptDecrypt_PGP(t *testing.T) { + const ( + mockPublicKey = "../pgp/testdata/public.gpg" + mockPrivateKey = "../pgp/testdata/private.gpg" + mockFingerprint = "B59DAF469E8C948138901A649732075EA221A7EA" + ) + + g := NewWithT(t) + + gnuPGHome, err := pgp.NewGnuPGHome() + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + g.Expect(gnuPGHome.ImportFile(mockPublicKey)).To(Succeed()) + + s := NewServer(WithGnuPGHome(gnuPGHome)) + key := KeyFromMasterKey(pgp.MasterKeyFromFingerprint(mockFingerprint)) + dataKey := []byte("some data key") + encResp, err := s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ + Key: &key, + Plaintext: dataKey, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(encResp.Ciphertext).ToNot(BeEmpty()) + g.Expect(encResp.Ciphertext).ToNot(Equal(dataKey)) + + g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) + decResp, err := s.Decrypt(context.TODO(), &keyservice.DecryptRequest{ + Key: &key, + Ciphertext: encResp.Ciphertext, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(decResp.Plaintext).To(Equal(dataKey)) +} + +func TestServer_EncryptDecrypt_age(t *testing.T) { + g := NewWithT(t) + + const ( + mockRecipient string = "age1lzd99uklcjnc0e7d860axevet2cz99ce9pq6tzuzd05l5nr28ams36nvun" + mockIdentity string = "AGE-SECRET-KEY-1G0Q5K9TV4REQ3ZSQRMTMG8NSWQGYT0T7TZ33RAZEE0GZYVZN0APSU24RK7" + ) + + s := NewServer() + key := KeyFromMasterKey(&age.MasterKey{Recipient: mockRecipient}) + dataKey := []byte("some data key") + encResp, err := s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ + Key: &key, + Plaintext: dataKey, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(encResp.Ciphertext).ToNot(BeEmpty()) + g.Expect(encResp.Ciphertext).ToNot(Equal(dataKey)) + + i := make(age.ParsedIdentities, 0) + g.Expect(i.Import(mockIdentity)).To(Succeed()) + + s = NewServer(WithAgeIdentities(i)) + decResp, err := s.Decrypt(context.TODO(), &keyservice.DecryptRequest{ + Key: &key, + Ciphertext: encResp.Ciphertext, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(decResp.Plaintext).To(Equal(dataKey)) +} + +func TestServer_EncryptDecrypt_HCVault(t *testing.T) { + g := NewWithT(t) + + s := NewServer(WithVaultToken("token")) + key := KeyFromMasterKey(hcvault.MasterKeyFromAddress("https://example.com", "engine-path", "key-name")) + _, err := s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ + Key: &key, + }) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("failed to encrypt sops data key to Vault transit backend")) + + _, err = s.Decrypt(context.TODO(), &keyservice.DecryptRequest{ + Key: &key, + }) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("failed to decrypt sops data key from Vault transit backend")) +} + +func TestServer_EncryptDecrypt_HCVault_Fallback(t *testing.T) { + g := NewWithT(t) + + fallback := NewMockKeyServer() + s := NewServer(WithDefaultServer{Server: fallback}) + + key := KeyFromMasterKey(hcvault.MasterKeyFromAddress("https://example.com", "engine-path", "key-name")) + encReq := &keyservice.EncryptRequest{ + Key: &key, + Plaintext: []byte("some data key"), + } + _, err := s.Encrypt(context.TODO(), encReq) + g.Expect(err).To(HaveOccurred()) + g.Expect(fallback.encryptReqs).To(HaveLen(1)) + g.Expect(fallback.encryptReqs).To(ContainElement(encReq)) + g.Expect(fallback.decryptReqs).To(HaveLen(0)) + + fallback = NewMockKeyServer() + s = NewServer(WithDefaultServer{Server: fallback}) + + decReq := &keyservice.DecryptRequest{ + Key: &key, + Ciphertext: []byte("some ciphertext"), + } + _, err = s.Decrypt(context.TODO(), decReq) + g.Expect(fallback.decryptReqs).To(HaveLen(1)) + g.Expect(fallback.decryptReqs).To(ContainElement(decReq)) + g.Expect(fallback.encryptReqs).To(HaveLen(0)) +} + +func TestServer_EncryptDecrypt_azkv(t *testing.T) { + g := NewWithT(t) + + identity, err := azidentity.NewDefaultAzureCredential(nil) + g.Expect(err).ToNot(HaveOccurred()) + s := NewServer(WithAzureToken{Token: azkv.NewToken(identity)}) + + key := KeyFromMasterKey(azkv.MasterKeyFromURL("", "", "")) + _, err = s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ + Key: &key, + }) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("failed to encrypt sops data key with Azure Key Vault")) + + _, err = s.Decrypt(context.TODO(), &keyservice.DecryptRequest{ + Key: &key, + }) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("failed to decrypt sops data key with Azure Key Vault")) + +} + +func TestServer_EncryptDecrypt_azkv_Fallback(t *testing.T) { + g := NewWithT(t) + + fallback := NewMockKeyServer() + s := NewServer(WithDefaultServer{Server: fallback}) + + key := KeyFromMasterKey(azkv.MasterKeyFromURL("", "", "")) + encReq := &keyservice.EncryptRequest{ + Key: &key, + Plaintext: []byte("some data key"), + } + _, err := s.Encrypt(context.TODO(), encReq) + g.Expect(err).To(HaveOccurred()) + g.Expect(fallback.encryptReqs).To(HaveLen(1)) + g.Expect(fallback.encryptReqs).To(ContainElement(encReq)) + g.Expect(fallback.decryptReqs).To(HaveLen(0)) + + fallback = NewMockKeyServer() + s = NewServer(WithDefaultServer{Server: fallback}) + + decReq := &keyservice.DecryptRequest{ + Key: &key, + Ciphertext: []byte("some ciphertext"), + } + _, err = s.Decrypt(context.TODO(), decReq) + g.Expect(fallback.decryptReqs).To(HaveLen(1)) + g.Expect(fallback.decryptReqs).To(ContainElement(decReq)) + g.Expect(fallback.encryptReqs).To(HaveLen(0)) +} + +func TestServer_EncryptDecrypt_Nil_KeyType(t *testing.T) { + g := NewWithT(t) + + s := NewServer(WithDefaultServer{NewMockKeyServer()}) + + expectErr := fmt.Errorf("must provide a key") + + _, err := s.Encrypt(context.TODO(), &keyservice.EncryptRequest{Key: &keyservice.Key{KeyType: nil}}) + g.Expect(err).To(Equal(expectErr)) + + _, err = s.Decrypt(context.TODO(), &keyservice.DecryptRequest{Key: &keyservice.Key{KeyType: nil}}) + g.Expect(err).To(Equal(expectErr)) +} diff --git a/internal/sops/keyservice/utils_test.go b/internal/sops/keyservice/utils_test.go new file mode 100644 index 000000000..945ef1d08 --- /dev/null +++ b/internal/sops/keyservice/utils_test.go @@ -0,0 +1,97 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package keyservice + +import ( + "context" + "fmt" + + "go.mozilla.org/sops/v3/keys" + "go.mozilla.org/sops/v3/keyservice" + + "github.com/fluxcd/kustomize-controller/internal/sops/age" + "github.com/fluxcd/kustomize-controller/internal/sops/azkv" + "github.com/fluxcd/kustomize-controller/internal/sops/hcvault" + "github.com/fluxcd/kustomize-controller/internal/sops/pgp" +) + +// KeyFromMasterKey converts a SOPS internal MasterKey to an RPC Key that can +// be serialized with Protocol Buffers. +func KeyFromMasterKey(k keys.MasterKey) keyservice.Key { + switch mk := k.(type) { + case *pgp.MasterKey: + return keyservice.Key{ + KeyType: &keyservice.Key_PgpKey{ + PgpKey: &keyservice.PgpKey{ + Fingerprint: mk.Fingerprint, + }, + }, + } + case *hcvault.MasterKey: + return keyservice.Key{ + KeyType: &keyservice.Key_VaultKey{ + VaultKey: &keyservice.VaultKey{ + VaultAddress: mk.VaultAddress, + EnginePath: mk.EnginePath, + KeyName: mk.KeyName, + }, + }, + } + case *azkv.MasterKey: + return keyservice.Key{ + KeyType: &keyservice.Key_AzureKeyvaultKey{ + AzureKeyvaultKey: &keyservice.AzureKeyVaultKey{ + VaultUrl: mk.VaultURL, + Name: mk.Name, + Version: mk.Version, + }, + }, + } + case *age.MasterKey: + return keyservice.Key{ + KeyType: &keyservice.Key_AgeKey{ + AgeKey: &keyservice.AgeKey{ + Recipient: mk.Recipient, + }, + }, + } + default: + panic(fmt.Sprintf("tried to convert unknown MasterKey type %T to keyservice.Key", mk)) + } +} + +type MockKeyServer struct { + encryptReqs []*keyservice.EncryptRequest + decryptReqs []*keyservice.DecryptRequest +} + +func NewMockKeyServer() *MockKeyServer { + return &MockKeyServer{ + encryptReqs: make([]*keyservice.EncryptRequest, 0), + decryptReqs: make([]*keyservice.DecryptRequest, 0), + } +} + +func (ks *MockKeyServer) Encrypt(_ context.Context, req *keyservice.EncryptRequest) (*keyservice.EncryptResponse, error) { + ks.encryptReqs = append(ks.encryptReqs, req) + return nil, fmt.Errorf("not actually implemented") +} + +func (ks *MockKeyServer) Decrypt(_ context.Context, req *keyservice.DecryptRequest) (*keyservice.DecryptResponse, error) { + ks.decryptReqs = append(ks.decryptReqs, req) + return nil, fmt.Errorf("not actually implemented") +}