|
@@ -0,0 +1,1030 @@
|
|
|
|
|
+/*
|
|
|
|
|
+Copyright © The ESO 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
|
|
|
|
|
+
|
|
|
|
|
+ https://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 certificatemanager
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "bytes"
|
|
|
|
|
+ "context"
|
|
|
|
|
+ "crypto/ecdsa"
|
|
|
|
|
+ "crypto/elliptic"
|
|
|
|
|
+ "crypto/rand"
|
|
|
|
|
+ "crypto/x509"
|
|
|
|
|
+ "crypto/x509/pkix"
|
|
|
|
|
+ "encoding/pem"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "math/big"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+ "time"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/aws/aws-sdk-go-v2/aws"
|
|
|
|
|
+ "github.com/aws/aws-sdk-go-v2/service/acm"
|
|
|
|
|
+ "github.com/aws/aws-sdk-go-v2/service/acm/types"
|
|
|
|
|
+ "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
|
|
|
|
|
+ rgtTypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
|
|
|
|
|
+ "github.com/aws/smithy-go"
|
|
|
|
|
+ "github.com/hashicorp/golang-lru/v2/expirable"
|
|
|
|
|
+ "github.com/youmark/pkcs8"
|
|
|
|
|
+ corev1 "k8s.io/api/core/v1"
|
|
|
|
|
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
|
|
|
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
|
+
|
|
|
|
|
+ esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
|
|
|
|
|
+ fakeacm "github.com/external-secrets/external-secrets/providers/v1/aws/certificatemanager/fake"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const (
|
|
|
|
|
+ testRemoteKey = "my-cert"
|
|
|
|
|
+ pemPrivateKey = "PRIVATE KEY"
|
|
|
|
|
+ errPushSecret = "PushSecret: %v"
|
|
|
|
|
+ errGetSecret = "GetSecret: %v"
|
|
|
|
|
+ errExpectedTag = "expected tag %q=%q, got %q"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type testCerts struct {
|
|
|
|
|
+ LeafPEM []byte
|
|
|
|
|
+ IntermediatePEM []byte
|
|
|
|
|
+ RootPEM []byte
|
|
|
|
|
+ PrivateKeyPEM []byte
|
|
|
|
|
+ TLSCrt []byte
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func generateTestCerts(t *testing.T) testCerts {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+
|
|
|
|
|
+ rootKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
|
|
|
+ rootTmpl := &x509.Certificate{
|
|
|
|
|
+ SerialNumber: big.NewInt(1),
|
|
|
|
|
+ Subject: pkix.Name{CommonName: "Test Root CA"},
|
|
|
|
|
+ NotBefore: time.Now().Add(-time.Hour),
|
|
|
|
|
+ NotAfter: time.Now().Add(24 * time.Hour),
|
|
|
|
|
+ IsCA: true,
|
|
|
|
|
+ BasicConstraintsValid: true,
|
|
|
|
|
+ }
|
|
|
|
|
+ rootDER, err := x509.CreateCertificate(rand.Reader, rootTmpl, rootTmpl, &rootKey.PublicKey, rootKey)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("create root cert: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ rootCert, _ := x509.ParseCertificate(rootDER)
|
|
|
|
|
+
|
|
|
|
|
+ interKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
|
|
|
+ interTmpl := &x509.Certificate{
|
|
|
|
|
+ SerialNumber: big.NewInt(2),
|
|
|
|
|
+ Subject: pkix.Name{CommonName: "Test Intermediate CA"},
|
|
|
|
|
+ NotBefore: time.Now().Add(-time.Hour),
|
|
|
|
|
+ NotAfter: time.Now().Add(24 * time.Hour),
|
|
|
|
|
+ IsCA: true,
|
|
|
|
|
+ BasicConstraintsValid: true,
|
|
|
|
|
+ SubjectKeyId: []byte{2},
|
|
|
|
|
+ AuthorityKeyId: rootCert.SubjectKeyId,
|
|
|
|
|
+ }
|
|
|
|
|
+ interDER, err := x509.CreateCertificate(rand.Reader, interTmpl, rootCert, &interKey.PublicKey, rootKey)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("create intermediate cert: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ interCert, _ := x509.ParseCertificate(interDER)
|
|
|
|
|
+
|
|
|
|
|
+ leafKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
|
|
|
+ leafTmpl := &x509.Certificate{
|
|
|
|
|
+ SerialNumber: big.NewInt(3),
|
|
|
|
|
+ Subject: pkix.Name{CommonName: "test.example.com"},
|
|
|
|
|
+ NotBefore: time.Now().Add(-time.Hour),
|
|
|
|
|
+ NotAfter: time.Now().Add(24 * time.Hour),
|
|
|
|
|
+ SubjectKeyId: []byte{3},
|
|
|
|
|
+ AuthorityKeyId: interCert.SubjectKeyId,
|
|
|
|
|
+ }
|
|
|
|
|
+ leafDER, err := x509.CreateCertificate(rand.Reader, leafTmpl, interCert, &leafKey.PublicKey, interKey)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("create leaf cert: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ leafPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafDER})
|
|
|
|
|
+ interPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: interDER})
|
|
|
|
|
+ rootPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootDER})
|
|
|
|
|
+
|
|
|
|
|
+ leafKeyDER, _ := x509.MarshalPKCS8PrivateKey(leafKey)
|
|
|
|
|
+ privKeyPEM := pem.EncodeToMemory(&pem.Block{Type: pemPrivateKey, Bytes: leafKeyDER})
|
|
|
|
|
+
|
|
|
|
|
+ tlsCrt := make([]byte, 0, len(leafPEM)+len(interPEM))
|
|
|
|
|
+ tlsCrt = append(tlsCrt, leafPEM...)
|
|
|
|
|
+ tlsCrt = append(tlsCrt, interPEM...)
|
|
|
|
|
+
|
|
|
|
|
+ return testCerts{
|
|
|
|
|
+ LeafPEM: leafPEM,
|
|
|
|
|
+ IntermediatePEM: interPEM,
|
|
|
|
|
+ RootPEM: rootPEM,
|
|
|
|
|
+ PrivateKeyPEM: privKeyPEM,
|
|
|
|
|
+ TLSCrt: tlsCrt,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type pushSecretData struct {
|
|
|
|
|
+ remoteKey string
|
|
|
|
|
+ secretKey string
|
|
|
|
|
+ property string
|
|
|
|
|
+ metadata *apiextensionsv1.JSON
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (p *pushSecretData) GetRemoteKey() string { return p.remoteKey }
|
|
|
|
|
+func (p *pushSecretData) GetSecretKey() string { return p.secretKey }
|
|
|
|
|
+func (p *pushSecretData) GetProperty() string { return p.property }
|
|
|
|
|
+func (p *pushSecretData) GetMetadata() *apiextensionsv1.JSON { return p.metadata }
|
|
|
|
|
+
|
|
|
|
|
+type remoteRef struct {
|
|
|
|
|
+ remoteKey string
|
|
|
|
|
+ property string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (r remoteRef) GetRemoteKey() string { return r.remoteKey }
|
|
|
|
|
+func (r remoteRef) GetProperty() string { return r.property }
|
|
|
|
|
+
|
|
|
|
|
+func tlsSecret(tlsCrt, tlsKey []byte) *corev1.Secret {
|
|
|
|
|
+ return &corev1.Secret{
|
|
|
|
|
+ ObjectMeta: metav1.ObjectMeta{Name: "my-tls", Namespace: "default"},
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "tls.crt": tlsCrt,
|
|
|
|
|
+ "tls.key": tlsKey,
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func managedTags(remoteKey string) []types.Tag {
|
|
|
|
|
+ return []types.Tag{
|
|
|
|
|
+ {Key: aws.String(managedBy), Value: aws.String(externalSecrets)},
|
|
|
|
|
+ {Key: aws.String(remoteKeyTag), Value: aws.String(remoteKey)},
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func newProvider(fc *fakeacm.Client) *CertificateManager {
|
|
|
|
|
+ return &CertificateManager{
|
|
|
|
|
+ client: fc,
|
|
|
|
|
+ rgtClient: stubRgt(""),
|
|
|
|
|
+ referentAuth: false,
|
|
|
|
|
+ cfg: &aws.Config{},
|
|
|
|
|
+ arnCache: expirable.NewLRU[string, string](arnCacheSize, nil, arnCacheTTL),
|
|
|
|
|
+ exportCache: expirable.NewLRU[string, exportCacheEntry](exportCacheSize, nil, exportCacheTTL),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func stubRgt(certARN string) *fakeacm.RgtClient {
|
|
|
|
|
+ return &fakeacm.RgtClient{
|
|
|
|
|
+ GetResourcesFn: func(_ context.Context, _ *resourcegroupstaggingapi.GetResourcesInput, _ ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
|
|
|
|
|
+ out := &resourcegroupstaggingapi.GetResourcesOutput{}
|
|
|
|
|
+ if certARN != "" {
|
|
|
|
|
+ out.ResourceTagMappingList = []rgtTypes.ResourceTagMapping{
|
|
|
|
|
+ {ResourceARN: aws.String(certARN)},
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return out, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func exportableDescribe() fakeacm.DescribeCertificateFn {
|
|
|
|
|
+ return func(_ context.Context, _ *acm.DescribeCertificateInput, _ ...func(*acm.Options)) (*acm.DescribeCertificateOutput, error) {
|
|
|
|
|
+ return &acm.DescribeCertificateOutput{
|
|
|
|
|
+ Certificate: &types.CertificateDetail{
|
|
|
|
|
+ Options: &types.CertificateOptions{
|
|
|
|
|
+ Export: types.CertificateExportEnabled,
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSplitCertificatePEM_LeafOnly(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+
|
|
|
|
|
+ leaf, chain, err := splitCertificatePEM(certs.LeafPEM)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("splitCertificatePEM: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !bytes.Equal(leaf, certs.LeafPEM) {
|
|
|
|
|
+ t.Error("leaf does not match input")
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(chain) != 0 {
|
|
|
|
|
+ t.Errorf("expected empty chain for leaf-only input, got %d bytes", len(chain))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSplitCertificatePEM_LeafAndIntermediate(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ leaf, chain, err := splitCertificatePEM(certs.TLSCrt)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("splitCertificatePEM: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !bytes.Equal(leaf, certs.LeafPEM) {
|
|
|
|
|
+ t.Error("leaf does not match first PEM block")
|
|
|
|
|
+ }
|
|
|
|
|
+ if !bytes.Equal(chain, certs.IntermediatePEM) {
|
|
|
|
|
+ t.Error("chain does not match intermediate PEM block")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSplitCertificatePEM_NoCertificates(t *testing.T) {
|
|
|
|
|
+ _, _, err := splitCertificatePEM([]byte("not a certificate"))
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected error for input with no CERTIFICATE blocks")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSplitCertificatePEM_InvalidLeafCertificate(t *testing.T) {
|
|
|
|
|
+ badPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: []byte("not valid DER")})
|
|
|
|
|
+ _, _, err := splitCertificatePEM(badPEM)
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected error for invalid leaf certificate DER")
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := err.Error(); !strings.Contains(got, "failed to parse leaf certificate") {
|
|
|
|
|
+ t.Errorf("expected error to mention leaf certificate, got: %s", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestPushSecret_NewCertificate(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/new"
|
|
|
|
|
+
|
|
|
|
|
+ var gotCert, gotChain []byte
|
|
|
|
|
+ var gotTags []types.Tag
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ ImportCertificateFn: func(_ context.Context, p *acm.ImportCertificateInput, _ ...func(*acm.Options)) (*acm.ImportCertificateOutput, error) {
|
|
|
|
|
+ if p.CertificateArn != nil {
|
|
|
|
|
+ t.Error("expected no CertificateArn on first import")
|
|
|
|
|
+ }
|
|
|
|
|
+ gotCert = p.Certificate
|
|
|
|
|
+ gotChain = p.CertificateChain
|
|
|
|
|
+ gotTags = p.Tags
|
|
|
|
|
+ return &acm.ImportCertificateOutput{CertificateArn: aws.String(arn)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ AddTagsToCertificateFn: func(_ context.Context, _ *acm.AddTagsToCertificateInput, _ ...func(*acm.Options)) (*acm.AddTagsToCertificateOutput, error) {
|
|
|
|
|
+ return &acm.AddTagsToCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ListTagsForCertificateOutput{Tags: managedTags(testRemoteKey)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ RemoveTagsFromCertificateFn: func(_ context.Context, _ *acm.RemoveTagsFromCertificateInput, _ ...func(*acm.Options)) (*acm.RemoveTagsFromCertificateOutput, error) {
|
|
|
|
|
+ return &acm.RemoveTagsFromCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ psd := &pushSecretData{remoteKey: testRemoteKey}
|
|
|
|
|
+ secret := tlsSecret(certs.TLSCrt, certs.PrivateKeyPEM)
|
|
|
|
|
+
|
|
|
|
|
+ if err := newProvider(fake).PushSecret(context.Background(), secret, psd); err != nil {
|
|
|
|
|
+ t.Fatalf(errPushSecret, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !bytes.Equal(gotCert, certs.LeafPEM) {
|
|
|
|
|
+ t.Error("Certificate field should be the leaf certificate only")
|
|
|
|
|
+ }
|
|
|
|
|
+ if !bytes.Equal(gotChain, certs.IntermediatePEM) {
|
|
|
|
|
+ t.Error("CertificateChain field should be the intermediate certificate")
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(gotTags) != 2 {
|
|
|
|
|
+ t.Fatalf("expected 2 management tags on new import, got %d", len(gotTags))
|
|
|
|
|
+ }
|
|
|
|
|
+ tagMap := make(map[string]string, len(gotTags))
|
|
|
|
|
+ for _, tag := range gotTags {
|
|
|
|
|
+ tagMap[aws.ToString(tag.Key)] = aws.ToString(tag.Value)
|
|
|
|
|
+ }
|
|
|
|
|
+ if tagMap[managedBy] != externalSecrets {
|
|
|
|
|
+ t.Errorf(errExpectedTag, managedBy, externalSecrets, tagMap[managedBy])
|
|
|
|
|
+ }
|
|
|
|
|
+ if tagMap[remoteKeyTag] != testRemoteKey {
|
|
|
|
|
+ t.Errorf(errExpectedTag, remoteKeyTag, testRemoteKey, tagMap[remoteKeyTag])
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestPushSecret_LeafOnly_NoChainSent(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/leaf-only"
|
|
|
|
|
+
|
|
|
|
|
+ var gotChain []byte
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ ImportCertificateFn: func(_ context.Context, p *acm.ImportCertificateInput, _ ...func(*acm.Options)) (*acm.ImportCertificateOutput, error) {
|
|
|
|
|
+ gotChain = p.CertificateChain
|
|
|
|
|
+ return &acm.ImportCertificateOutput{CertificateArn: aws.String(arn)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ AddTagsToCertificateFn: func(_ context.Context, _ *acm.AddTagsToCertificateInput, _ ...func(*acm.Options)) (*acm.AddTagsToCertificateOutput, error) {
|
|
|
|
|
+ return &acm.AddTagsToCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ListTagsForCertificateOutput{Tags: managedTags(testRemoteKey)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ RemoveTagsFromCertificateFn: func(_ context.Context, _ *acm.RemoveTagsFromCertificateInput, _ ...func(*acm.Options)) (*acm.RemoveTagsFromCertificateOutput, error) {
|
|
|
|
|
+ return &acm.RemoveTagsFromCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ psd := &pushSecretData{remoteKey: testRemoteKey}
|
|
|
|
|
+ secret := tlsSecret(certs.LeafPEM, certs.PrivateKeyPEM)
|
|
|
|
|
+
|
|
|
|
|
+ if err := newProvider(fake).PushSecret(context.Background(), secret, psd); err != nil {
|
|
|
|
|
+ t.Fatalf(errPushSecret, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(gotChain) != 0 {
|
|
|
|
|
+ t.Error("CertificateChain should be nil when tls.crt contains only the leaf")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestPushSecret_ReimportExisting(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/existing"
|
|
|
|
|
+ var importedARN string
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ GetCertificateFn: func(_ context.Context, _ *acm.GetCertificateInput, _ ...func(*acm.Options)) (*acm.GetCertificateOutput, error) {
|
|
|
|
|
+ return &acm.GetCertificateOutput{Certificate: aws.String(string(certs.RootPEM))}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ListTagsForCertificateOutput{Tags: managedTags(testRemoteKey)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ImportCertificateFn: func(_ context.Context, p *acm.ImportCertificateInput, _ ...func(*acm.Options)) (*acm.ImportCertificateOutput, error) {
|
|
|
|
|
+ importedARN = aws.ToString(p.CertificateArn)
|
|
|
|
|
+ if len(p.Tags) > 0 {
|
|
|
|
|
+ t.Error("Tags must not be set on re-import")
|
|
|
|
|
+ }
|
|
|
|
|
+ return &acm.ImportCertificateOutput{CertificateArn: p.CertificateArn}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ AddTagsToCertificateFn: func(_ context.Context, _ *acm.AddTagsToCertificateInput, _ ...func(*acm.Options)) (*acm.AddTagsToCertificateOutput, error) {
|
|
|
|
|
+ return &acm.AddTagsToCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ RemoveTagsFromCertificateFn: func(_ context.Context, _ *acm.RemoveTagsFromCertificateInput, _ ...func(*acm.Options)) (*acm.RemoveTagsFromCertificateOutput, error) {
|
|
|
|
|
+ return &acm.RemoveTagsFromCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ psd := &pushSecretData{remoteKey: testRemoteKey}
|
|
|
|
|
+ secret := tlsSecret(certs.TLSCrt, certs.PrivateKeyPEM)
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.rgtClient = stubRgt(arn)
|
|
|
|
|
+ if err := provider.PushSecret(context.Background(), secret, psd); err != nil {
|
|
|
|
|
+ t.Fatalf(errPushSecret, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if importedARN != arn {
|
|
|
|
|
+ t.Errorf("expected CertificateArn %q, got %q", arn, importedARN)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestPushSecret_SkipsReimportWhenUnchanged(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/unchanged"
|
|
|
|
|
+
|
|
|
|
|
+ importCalled := false
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ GetCertificateFn: func(_ context.Context, _ *acm.GetCertificateInput, _ ...func(*acm.Options)) (*acm.GetCertificateOutput, error) {
|
|
|
|
|
+ return &acm.GetCertificateOutput{
|
|
|
|
|
+ Certificate: aws.String(string(certs.LeafPEM)),
|
|
|
|
|
+ CertificateChain: aws.String(string(certs.IntermediatePEM)),
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ListTagsForCertificateOutput{Tags: managedTags(testRemoteKey)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ImportCertificateFn: func(_ context.Context, _ *acm.ImportCertificateInput, _ ...func(*acm.Options)) (*acm.ImportCertificateOutput, error) {
|
|
|
|
|
+ importCalled = true
|
|
|
|
|
+ t.Error("ImportCertificate should not be called when content is unchanged")
|
|
|
|
|
+ return &acm.ImportCertificateOutput{CertificateArn: aws.String(arn)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ AddTagsToCertificateFn: func(_ context.Context, _ *acm.AddTagsToCertificateInput, _ ...func(*acm.Options)) (*acm.AddTagsToCertificateOutput, error) {
|
|
|
|
|
+ return &acm.AddTagsToCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ RemoveTagsFromCertificateFn: func(_ context.Context, _ *acm.RemoveTagsFromCertificateInput, _ ...func(*acm.Options)) (*acm.RemoveTagsFromCertificateOutput, error) {
|
|
|
|
|
+ return &acm.RemoveTagsFromCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ psd := &pushSecretData{remoteKey: testRemoteKey}
|
|
|
|
|
+ secret := tlsSecret(certs.TLSCrt, certs.PrivateKeyPEM)
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.rgtClient = stubRgt(arn)
|
|
|
|
|
+ if err := provider.PushSecret(context.Background(), secret, psd); err != nil {
|
|
|
|
|
+ t.Fatalf(errPushSecret, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if importCalled {
|
|
|
|
|
+ t.Error("ImportCertificate was called despite unchanged content")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestPushSecret_MissingCertKey(t *testing.T) {
|
|
|
|
|
+ fake := &fakeacm.Client{}
|
|
|
|
|
+
|
|
|
|
|
+ psd := &pushSecretData{remoteKey: testRemoteKey}
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ ObjectMeta: metav1.ObjectMeta{Name: "bad-secret", Namespace: "default"},
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "tls.key": []byte("KEY"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if err := newProvider(fake).PushSecret(context.Background(), secret, psd); err == nil {
|
|
|
|
|
+ t.Fatal("expected error when tls.crt is missing")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestPushSecret_SecretKeyNotEmpty_Rejected(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+
|
|
|
|
|
+ psd := &pushSecretData{
|
|
|
|
|
+ remoteKey: testRemoteKey,
|
|
|
|
|
+ secretKey: "tls.crt",
|
|
|
|
|
+ }
|
|
|
|
|
+ secret := tlsSecret(certs.TLSCrt, certs.PrivateKeyPEM)
|
|
|
|
|
+
|
|
|
|
|
+ err := newProvider(&fakeacm.Client{}).PushSecret(context.Background(), secret, psd)
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected error when secretKey is non-empty")
|
|
|
|
|
+ }
|
|
|
|
|
+ if err.Error() != errSecretKeyNotEmpty {
|
|
|
|
|
+ t.Errorf("expected error %q, got %q", errSecretKeyNotEmpty, err.Error())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestPushSecret_CachePreventsDoubleImport(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/cached"
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ ImportCertificateFn: func(_ context.Context, _ *acm.ImportCertificateInput, _ ...func(*acm.Options)) (*acm.ImportCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ImportCertificateOutput{CertificateArn: aws.String(arn)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ GetCertificateFn: func(_ context.Context, _ *acm.GetCertificateInput, _ ...func(*acm.Options)) (*acm.GetCertificateOutput, error) {
|
|
|
|
|
+ return &acm.GetCertificateOutput{
|
|
|
|
|
+ Certificate: aws.String(string(certs.LeafPEM)),
|
|
|
|
|
+ CertificateChain: aws.String(string(certs.IntermediatePEM)),
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ AddTagsToCertificateFn: func(_ context.Context, _ *acm.AddTagsToCertificateInput, _ ...func(*acm.Options)) (*acm.AddTagsToCertificateOutput, error) {
|
|
|
|
|
+ return &acm.AddTagsToCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, p *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ if aws.ToString(p.CertificateArn) == arn {
|
|
|
|
|
+ return &acm.ListTagsForCertificateOutput{Tags: managedTags(testRemoteKey)}, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ return &acm.ListTagsForCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ RemoveTagsFromCertificateFn: func(_ context.Context, _ *acm.RemoveTagsFromCertificateInput, _ ...func(*acm.Options)) (*acm.RemoveTagsFromCertificateOutput, error) {
|
|
|
|
|
+ return &acm.RemoveTagsFromCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tagLookupCount := 0
|
|
|
|
|
+ fakeRgt := &fakeacm.RgtClient{
|
|
|
|
|
+ GetResourcesFn: func(_ context.Context, _ *resourcegroupstaggingapi.GetResourcesInput, _ ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
|
|
|
|
|
+ tagLookupCount++
|
|
|
|
|
+ return &resourcegroupstaggingapi.GetResourcesOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ psd := &pushSecretData{remoteKey: testRemoteKey}
|
|
|
|
|
+ secret := tlsSecret(certs.TLSCrt, certs.PrivateKeyPEM)
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.rgtClient = fakeRgt
|
|
|
|
|
+ if err := provider.PushSecret(context.Background(), secret, psd); err != nil {
|
|
|
|
|
+ t.Fatalf("first PushSecret: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if tagLookupCount != 1 {
|
|
|
|
|
+ t.Fatalf("expected GetResources to be called once on first push, got %d", tagLookupCount)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Second push on the same provider: cached ARN must skip the lookup via RGT API.
|
|
|
|
|
+ if err := provider.PushSecret(context.Background(), secret, psd); err != nil {
|
|
|
|
|
+ t.Fatalf("second PushSecret: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if tagLookupCount != 1 {
|
|
|
|
|
+ t.Errorf("expected cached ARN to skip GetResources, got %d lookups", tagLookupCount)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestPushSecret_CacheClearedOnDelete(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/to-delete-cache"
|
|
|
|
|
+
|
|
|
|
|
+ importCount := 0
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ GetCertificateFn: func(_ context.Context, _ *acm.GetCertificateInput, _ ...func(*acm.Options)) (*acm.GetCertificateOutput, error) {
|
|
|
|
|
+ return &acm.GetCertificateOutput{Certificate: aws.String(string(certs.RootPEM))}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ImportCertificateFn: func(_ context.Context, p *acm.ImportCertificateInput, _ ...func(*acm.Options)) (*acm.ImportCertificateOutput, error) {
|
|
|
|
|
+ importCount++
|
|
|
|
|
+ return &acm.ImportCertificateOutput{CertificateArn: aws.String(arn)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ AddTagsToCertificateFn: func(_ context.Context, _ *acm.AddTagsToCertificateInput, _ ...func(*acm.Options)) (*acm.AddTagsToCertificateOutput, error) {
|
|
|
|
|
+ return &acm.AddTagsToCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ListTagsForCertificateOutput{Tags: managedTags(testRemoteKey)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ RemoveTagsFromCertificateFn: func(_ context.Context, _ *acm.RemoveTagsFromCertificateInput, _ ...func(*acm.Options)) (*acm.RemoveTagsFromCertificateOutput, error) {
|
|
|
|
|
+ return &acm.RemoveTagsFromCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ DeleteCertificateFn: func(_ context.Context, _ *acm.DeleteCertificateInput, _ ...func(*acm.Options)) (*acm.DeleteCertificateOutput, error) {
|
|
|
|
|
+ return &acm.DeleteCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ psd := &pushSecretData{remoteKey: testRemoteKey}
|
|
|
|
|
+ secret := tlsSecret(certs.TLSCrt, certs.PrivateKeyPEM)
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.rgtClient = stubRgt(arn)
|
|
|
|
|
+ if err := provider.PushSecret(context.Background(), secret, psd); err != nil {
|
|
|
|
|
+ t.Fatalf(errPushSecret, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, ok := provider.arnCache.Get(testRemoteKey); !ok {
|
|
|
|
|
+ t.Fatal("expected ARN to be cached after PushSecret")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if err := provider.DeleteSecret(context.Background(), remoteRef{remoteKey: testRemoteKey}); err != nil {
|
|
|
|
|
+ t.Fatalf("DeleteSecret: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, ok := provider.arnCache.Get(testRemoteKey); ok {
|
|
|
|
|
+ t.Error("expected ARN cache to be cleared after DeleteSecret")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSecretExists_Found(t *testing.T) {
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/abc"
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ListTagsForCertificateOutput{Tags: managedTags(testRemoteKey)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.rgtClient = stubRgt(arn)
|
|
|
|
|
+ exists, err := provider.SecretExists(context.Background(), remoteRef{remoteKey: testRemoteKey})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("SecretExists: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !exists {
|
|
|
|
|
+ t.Error("expected certificate to exist")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSecretExists_StaleCacheEvictedOnNotFound(t *testing.T) {
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/stale"
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ return nil, &smithyFakeNotFound{}
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ fakeRgt := &fakeacm.RgtClient{
|
|
|
|
|
+ GetResourcesFn: func(_ context.Context, _ *resourcegroupstaggingapi.GetResourcesInput, _ ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
|
|
|
|
|
+ t.Error("GetResources must not be called when the ARN is served from cache")
|
|
|
|
|
+ return &resourcegroupstaggingapi.GetResourcesOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.rgtClient = fakeRgt
|
|
|
|
|
+ provider.arnCache.Add(testRemoteKey, arn)
|
|
|
|
|
+
|
|
|
|
|
+ exists, err := provider.SecretExists(context.Background(), remoteRef{remoteKey: testRemoteKey})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("SecretExists: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if exists {
|
|
|
|
|
+ t.Error("expected SecretExists to report false when the cert is gone")
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, ok := provider.arnCache.Get(testRemoteKey); ok {
|
|
|
|
|
+ t.Error("expected stale cache entry to be evicted")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSearchCertificatesByTag_Duplicates(t *testing.T) {
|
|
|
|
|
+ const (
|
|
|
|
|
+ arnA = "arn:aws:acm:us-east-1:123456789012:certificate/aaa"
|
|
|
|
|
+ arnB = "arn:aws:acm:us-east-1:123456789012:certificate/bbb"
|
|
|
|
|
+ arnC = "arn:aws:acm:us-east-1:123456789012:certificate/ccc"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ call := 0
|
|
|
|
|
+ fakeRgt := &fakeacm.RgtClient{
|
|
|
|
|
+ GetResourcesFn: func(_ context.Context, in *resourcegroupstaggingapi.GetResourcesInput, _ ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
|
|
|
|
|
+ call++
|
|
|
|
|
+ switch call {
|
|
|
|
|
+ case 1:
|
|
|
|
|
+ if in.PaginationToken != nil {
|
|
|
|
|
+ t.Errorf("expected no pagination token on first call, got %q", *in.PaginationToken)
|
|
|
|
|
+ }
|
|
|
|
|
+ next := "page2"
|
|
|
|
|
+ return &resourcegroupstaggingapi.GetResourcesOutput{
|
|
|
|
|
+ ResourceTagMappingList: []rgtTypes.ResourceTagMapping{
|
|
|
|
|
+ {ResourceARN: aws.String(arnC)},
|
|
|
|
|
+ {ResourceARN: aws.String(arnA)},
|
|
|
|
|
+ },
|
|
|
|
|
+ PaginationToken: &next,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ case 2:
|
|
|
|
|
+ if in.PaginationToken == nil || *in.PaginationToken != "page2" {
|
|
|
|
|
+ t.Errorf("expected pagination token %q on second call, got %v", "page2", in.PaginationToken)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &resourcegroupstaggingapi.GetResourcesOutput{
|
|
|
|
|
+ ResourceTagMappingList: []rgtTypes.ResourceTagMapping{
|
|
|
|
|
+ {ResourceARN: aws.String(arnB)},
|
|
|
|
|
+ },
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Fatalf("unexpected extra GetResources call: %d", call)
|
|
|
|
|
+ return nil, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(&fakeacm.Client{})
|
|
|
|
|
+ provider.rgtClient = fakeRgt
|
|
|
|
|
+
|
|
|
|
|
+ got, err := provider.searchCertificatesByTag(context.Background(), testRemoteKey)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("searchCertificatesByTag: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got != arnA {
|
|
|
|
|
+ t.Errorf("expected lexically smallest ARN %q, got %q", arnA, got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if call != 2 {
|
|
|
|
|
+ t.Errorf("expected 2 GetResources calls (full pagination), got %d", call)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSecretExists_NotFound(t *testing.T) {
|
|
|
|
|
+ exists, err := newProvider(&fakeacm.Client{}).SecretExists(context.Background(), remoteRef{remoteKey: "missing"})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("SecretExists: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if exists {
|
|
|
|
|
+ t.Error("expected certificate to not exist")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestDeleteSecret_Managed(t *testing.T) {
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/to-delete"
|
|
|
|
|
+ deleted := false
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ListTagsForCertificateOutput{Tags: managedTags(testRemoteKey)}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ DeleteCertificateFn: func(_ context.Context, p *acm.DeleteCertificateInput, _ ...func(*acm.Options)) (*acm.DeleteCertificateOutput, error) {
|
|
|
|
|
+ if aws.ToString(p.CertificateArn) != arn {
|
|
|
|
|
+ t.Errorf("expected ARN %q, got %q", arn, aws.ToString(p.CertificateArn))
|
|
|
|
|
+ }
|
|
|
|
|
+ deleted = true
|
|
|
|
|
+ return &acm.DeleteCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.rgtClient = stubRgt(arn)
|
|
|
|
|
+ if err := provider.DeleteSecret(context.Background(), remoteRef{remoteKey: testRemoteKey}); err != nil {
|
|
|
|
|
+ t.Fatalf("DeleteSecret: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !deleted {
|
|
|
|
|
+ t.Error("expected DeleteCertificate to be called")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestDeleteSecret_NotFound(t *testing.T) {
|
|
|
|
|
+ if err := newProvider(&fakeacm.Client{}).DeleteSecret(context.Background(), remoteRef{remoteKey: "gone"}); err != nil {
|
|
|
|
|
+ t.Fatalf("DeleteSecret on missing cert: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestDeleteSecret_NotManagedByESO(t *testing.T) {
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/external"
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ListTagsForCertificateOutput{Tags: []types.Tag{
|
|
|
|
|
+ {Key: aws.String(remoteKeyTag), Value: aws.String("ext-cert")},
|
|
|
|
|
+ }}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.rgtClient = stubRgt(arn)
|
|
|
|
|
+ err := provider.DeleteSecret(context.Background(), remoteRef{remoteKey: "ext-cert"})
|
|
|
|
|
+ if err == nil || err.Error() != errNotManagedByESO {
|
|
|
|
|
+ t.Fatalf("expected %q, got: %v", errNotManagedByESO, err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestDeleteSecret_DeletedBetweenFindAndListTags(t *testing.T) {
|
|
|
|
|
+ const arn = "arn:aws:acm:us-east-1:123456789012:certificate/race"
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ ListTagsForCertificateFn: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) {
|
|
|
|
|
+ return nil, &smithyFakeNotFound{}
|
|
|
|
|
+ },
|
|
|
|
|
+ DeleteCertificateFn: func(_ context.Context, _ *acm.DeleteCertificateInput, _ ...func(*acm.Options)) (*acm.DeleteCertificateOutput, error) {
|
|
|
|
|
+ t.Error("DeleteCertificate should not be called when cert is already gone")
|
|
|
|
|
+ return &acm.DeleteCertificateOutput{}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.rgtClient = stubRgt(arn)
|
|
|
|
|
+ if err := provider.DeleteSecret(context.Background(), remoteRef{remoteKey: testRemoteKey}); err != nil {
|
|
|
|
|
+ t.Fatalf("expected no-op when cert disappears between find and verify, got: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type smithyFakeNotFound struct{}
|
|
|
|
|
+
|
|
|
|
|
+func (e *smithyFakeNotFound) Error() string { return errResourceNotFound }
|
|
|
|
|
+func (e *smithyFakeNotFound) ErrorCode() string { return errResourceNotFound }
|
|
|
|
|
+func (e *smithyFakeNotFound) ErrorMessage() string { return "certificate not found" }
|
|
|
|
|
+func (e *smithyFakeNotFound) ErrorFault() smithy.ErrorFault { return smithy.FaultClient }
|
|
|
|
|
+
|
|
|
|
|
+func encryptPKCS8ForTest(t *testing.T, privateDER, passphrase []byte) []byte {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+
|
|
|
|
|
+ privKey, err := x509.ParsePKCS8PrivateKey(privateDER)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("parse pkcs8: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ encryptedDER, err := pkcs8.MarshalPrivateKey(privKey, passphrase, nil)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("encrypt pkcs8: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return pem.EncodeToMemory(&pem.Block{Type: "ENCRYPTED PRIVATE KEY", Bytes: encryptedDER})
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetSecret_ReturnsConcatenatedBundle(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+
|
|
|
|
|
+ pkcs8DER, _ := x509.MarshalPKCS8PrivateKey(func() *ecdsa.PrivateKey {
|
|
|
|
|
+ key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
|
|
|
+ return key
|
|
|
|
|
+ }())
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ GetCertificateFn: func(_ context.Context, _ *acm.GetCertificateInput, _ ...func(*acm.Options)) (*acm.GetCertificateOutput, error) {
|
|
|
|
|
+ return &acm.GetCertificateOutput{
|
|
|
|
|
+ Certificate: aws.String(string(certs.LeafPEM)),
|
|
|
|
|
+ CertificateChain: aws.String(string(certs.IntermediatePEM)),
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ DescribeCertificateFn: exportableDescribe(),
|
|
|
|
|
+ ExportCertificateFn: func(_ context.Context, p *acm.ExportCertificateInput, _ ...func(*acm.Options)) (*acm.ExportCertificateOutput, error) {
|
|
|
|
|
+ encPEM := encryptPKCS8ForTest(t, pkcs8DER, p.Passphrase)
|
|
|
|
|
+ return &acm.ExportCertificateOutput{
|
|
|
|
|
+ Certificate: aws.String(string(certs.LeafPEM)),
|
|
|
|
|
+ CertificateChain: aws.String(string(certs.IntermediatePEM)),
|
|
|
|
|
+ PrivateKey: aws.String(string(encPEM)),
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result, err := newProvider(fake).GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{
|
|
|
|
|
+ Key: "arn:aws:acm:us-east-1:123456789012:certificate/test",
|
|
|
|
|
+ })
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf(errGetSecret, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(string(result), "-----BEGIN CERTIFICATE-----") {
|
|
|
|
|
+ t.Error("expected certificate in output")
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(string(result), "-----BEGIN PRIVATE KEY-----") {
|
|
|
|
|
+ t.Error("expected decrypted private key in output")
|
|
|
|
|
+ }
|
|
|
|
|
+ if strings.Contains(string(result), "ENCRYPTED") {
|
|
|
|
|
+ t.Error("private key should be decrypted")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetSecret_CertOnly(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ GetCertificateFn: func(_ context.Context, _ *acm.GetCertificateInput, _ ...func(*acm.Options)) (*acm.GetCertificateOutput, error) {
|
|
|
|
|
+ return &acm.GetCertificateOutput{Certificate: aws.String(string(certs.LeafPEM))}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ DescribeCertificateFn: exportableDescribe(),
|
|
|
|
|
+ ExportCertificateFn: func(_ context.Context, _ *acm.ExportCertificateInput, _ ...func(*acm.Options)) (*acm.ExportCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ExportCertificateOutput{
|
|
|
|
|
+ Certificate: aws.String(string(certs.LeafPEM)),
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result, err := newProvider(fake).GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{
|
|
|
|
|
+ Key: "arn:aws:acm:us-east-1:123456789012:certificate/test",
|
|
|
|
|
+ })
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf(errGetSecret, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !bytes.Equal(result, certs.LeafPEM) {
|
|
|
|
|
+ t.Error("expected only the leaf certificate")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetSecret_EmptyARN(t *testing.T) {
|
|
|
|
|
+ _, err := newProvider(&fakeacm.Client{}).GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{})
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected error for empty ARN")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetSecret_NotExportable(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ exportCalled := false
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ GetCertificateFn: func(_ context.Context, _ *acm.GetCertificateInput, _ ...func(*acm.Options)) (*acm.GetCertificateOutput, error) {
|
|
|
|
|
+ return &acm.GetCertificateOutput{Certificate: aws.String(string(certs.LeafPEM))}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ DescribeCertificateFn: func(_ context.Context, _ *acm.DescribeCertificateInput, _ ...func(*acm.Options)) (*acm.DescribeCertificateOutput, error) {
|
|
|
|
|
+ return &acm.DescribeCertificateOutput{
|
|
|
|
|
+ Certificate: &types.CertificateDetail{
|
|
|
|
|
+ Options: &types.CertificateOptions{
|
|
|
|
|
+ Export: types.CertificateExportDisabled,
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ExportCertificateFn: func(_ context.Context, _ *acm.ExportCertificateInput, _ ...func(*acm.Options)) (*acm.ExportCertificateOutput, error) {
|
|
|
|
|
+ exportCalled = true
|
|
|
|
|
+ return nil, &smithyFakeValidation{}
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _, err := newProvider(fake).GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{
|
|
|
|
|
+ Key: "arn:aws:acm:us-east-1:123456789012:certificate/public",
|
|
|
|
|
+ })
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected error for non-exportable certificate")
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(err.Error(), "not exportable") {
|
|
|
|
|
+ t.Errorf("expected not-exportable error message, got: %s", err.Error())
|
|
|
|
|
+ }
|
|
|
|
|
+ if exportCalled {
|
|
|
|
|
+ t.Error("ExportCertificate should not be called for non-exportable certificates")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetSecret_NotExportable_NilOptions(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ GetCertificateFn: func(_ context.Context, _ *acm.GetCertificateInput, _ ...func(*acm.Options)) (*acm.GetCertificateOutput, error) {
|
|
|
|
|
+ return &acm.GetCertificateOutput{Certificate: aws.String(string(certs.LeafPEM))}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ DescribeCertificateFn: func(_ context.Context, _ *acm.DescribeCertificateInput, _ ...func(*acm.Options)) (*acm.DescribeCertificateOutput, error) {
|
|
|
|
|
+ return &acm.DescribeCertificateOutput{
|
|
|
|
|
+ Certificate: &types.CertificateDetail{},
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _, err := newProvider(fake).GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{
|
|
|
|
|
+ Key: "arn:aws:acm:us-east-1:123456789012:certificate/no-options",
|
|
|
|
|
+ })
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected error when Options is nil")
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(err.Error(), "not exportable") {
|
|
|
|
|
+ t.Errorf("expected not-exportable error, got: %s", err.Error())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetSecret_CacheHit(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ const certARN = "arn:aws:acm:us-east-1:123456789012:certificate/cached"
|
|
|
|
|
+ cachedPEM := []byte("cached-pem-bundle")
|
|
|
|
|
+
|
|
|
|
|
+ fingerprint := computeCertFingerprint(string(certs.LeafPEM), string(certs.IntermediatePEM))
|
|
|
|
|
+
|
|
|
|
|
+ exportCalled := false
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ GetCertificateFn: func(_ context.Context, _ *acm.GetCertificateInput, _ ...func(*acm.Options)) (*acm.GetCertificateOutput, error) {
|
|
|
|
|
+ return &acm.GetCertificateOutput{
|
|
|
|
|
+ Certificate: aws.String(string(certs.LeafPEM)),
|
|
|
|
|
+ CertificateChain: aws.String(string(certs.IntermediatePEM)),
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ ExportCertificateFn: func(_ context.Context, _ *acm.ExportCertificateInput, _ ...func(*acm.Options)) (*acm.ExportCertificateOutput, error) {
|
|
|
|
|
+ exportCalled = true
|
|
|
|
|
+ return nil, fmt.Errorf("should not be called")
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.exportCache.Add(certARN, exportCacheEntry{fingerprint: fingerprint, pem: cachedPEM})
|
|
|
|
|
+
|
|
|
|
|
+ result, err := provider.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{
|
|
|
|
|
+ Key: certARN,
|
|
|
|
|
+ })
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf(errGetSecret, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !bytes.Equal(result, cachedPEM) {
|
|
|
|
|
+ t.Errorf("expected cached PEM, got %q", string(result))
|
|
|
|
|
+ }
|
|
|
|
|
+ if exportCalled {
|
|
|
|
|
+ t.Error("ExportCertificate should not be called on cache hit")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetSecret_CacheMissOnFingerprintChange(t *testing.T) {
|
|
|
|
|
+ certs := generateTestCerts(t)
|
|
|
|
|
+ const certARN = "arn:aws:acm:us-east-1:123456789012:certificate/renewed"
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeacm.Client{
|
|
|
|
|
+ GetCertificateFn: func(_ context.Context, _ *acm.GetCertificateInput, _ ...func(*acm.Options)) (*acm.GetCertificateOutput, error) {
|
|
|
|
|
+ return &acm.GetCertificateOutput{Certificate: aws.String(string(certs.LeafPEM))}, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ DescribeCertificateFn: exportableDescribe(),
|
|
|
|
|
+ ExportCertificateFn: func(_ context.Context, _ *acm.ExportCertificateInput, _ ...func(*acm.Options)) (*acm.ExportCertificateOutput, error) {
|
|
|
|
|
+ return &acm.ExportCertificateOutput{
|
|
|
|
|
+ Certificate: aws.String(string(certs.LeafPEM)),
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ provider := newProvider(fake)
|
|
|
|
|
+ provider.exportCache.Add(certARN, exportCacheEntry{fingerprint: "old-fingerprint", pem: []byte("old-pem")})
|
|
|
|
|
+
|
|
|
|
|
+ result, err := provider.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{
|
|
|
|
|
+ Key: certARN,
|
|
|
|
|
+ })
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf(errGetSecret, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !bytes.Equal(result, certs.LeafPEM) {
|
|
|
|
|
+ t.Error("expected fresh export after fingerprint change")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cached, ok := provider.exportCache.Get(certARN)
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ t.Fatal("expected cache to be updated after export")
|
|
|
|
|
+ }
|
|
|
|
|
+ expectedFingerprint := computeCertFingerprint(string(certs.LeafPEM), "")
|
|
|
|
|
+ if cached.fingerprint != expectedFingerprint {
|
|
|
|
|
+ t.Error("cached fingerprint should be updated")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestDecryptPKCS8PEM_Roundtrip(t *testing.T) {
|
|
|
|
|
+ key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("generate key: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ pkcs8DER, err := x509.MarshalPKCS8PrivateKey(key)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("marshal pkcs8: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ passphrase := []byte("roundtrip-test-passphrase")
|
|
|
|
|
+ encPEM := encryptPKCS8ForTest(t, pkcs8DER, passphrase)
|
|
|
|
|
+
|
|
|
|
|
+ decPEM, err := decryptPKCS8PEM(encPEM, passphrase)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("decryptPKCS8PEM: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ block, _ := pem.Decode(decPEM)
|
|
|
|
|
+ if block == nil || block.Type != pemPrivateKey {
|
|
|
|
|
+ t.Fatalf("expected PRIVATE KEY PEM block, got %v", block)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, err := x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
|
|
|
|
|
+ t.Fatalf("decrypted key is not valid PKCS#8: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestDecryptPKCS8PEM_AlreadyUnencrypted(t *testing.T) {
|
|
|
|
|
+ key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
|
|
|
+ pkcs8DER, _ := x509.MarshalPKCS8PrivateKey(key)
|
|
|
|
|
+ unencPEM := pem.EncodeToMemory(&pem.Block{Type: pemPrivateKey, Bytes: pkcs8DER})
|
|
|
|
|
+
|
|
|
|
|
+ result, err := decryptPKCS8PEM(unencPEM, []byte("unused"))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("decryptPKCS8PEM: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !bytes.Equal(result, unencPEM) {
|
|
|
|
|
+ t.Error("unencrypted PEM should be returned as-is")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type smithyFakeValidation struct{}
|
|
|
|
|
+
|
|
|
|
|
+func (e *smithyFakeValidation) Error() string { return "ValidationException" }
|
|
|
|
|
+func (e *smithyFakeValidation) ErrorCode() string { return "ValidationException" }
|
|
|
|
|
+func (e *smithyFakeValidation) ErrorMessage() string { return "certificate is not exportable" }
|
|
|
|
|
+func (e *smithyFakeValidation) ErrorFault() smithy.ErrorFault { return smithy.FaultClient }
|