Skip to content

Commit

Permalink
sops/keyservice: tidy and add tests
Browse files Browse the repository at this point in the history
Signed-off-by: Hidde Beydals <hello@hidde.co>
  • Loading branch information
hiddeco committed Apr 4, 2022
1 parent b66715d commit 2aecad8
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 18 deletions.
2 changes: 1 addition & 1 deletion internal/sops/azkv/keysource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
45 changes: 28 additions & 17 deletions internal/sops/keyservice/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
214 changes: 214 additions & 0 deletions internal/sops/keyservice/server_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
Loading

0 comments on commit 2aecad8

Please sign in to comment.