sops/keyservice: tidy and add tests

Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
Hidde Beydals 2022-04-04 23:51:07 +02:00
parent ffdda3f3da
commit 9db141d9db
4 changed files with 340 additions and 18 deletions

View File

@ -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 {

View File

@ -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,

View File

@ -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))
}

View File

@ -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")
}