[OOM-K8S-CERT-EXTERNAL-PROVIDER] Add client for CertService API 47/114147/5
authorRemigiusz Janeczek <remigiusz.janeczek@nokia.com>
Thu, 22 Oct 2020 07:18:12 +0000 (09:18 +0200)
committerRemigiusz Janeczek <remigiusz.janeczek@nokia.com>
Thu, 22 Oct 2020 16:00:36 +0000 (16:00 +0000)
Issue-ID: OOM-2559
Signed-off-by: Remigiusz Janeczek <remigiusz.janeczek@nokia.com>
Change-Id: I3bf6c36b9eec7a661202b18eb7765e332ccfbc07

15 files changed:
certServiceK8sExternalProvider/deploy/_certificate_example_.yaml
certServiceK8sExternalProvider/src/certserviceclient/cert_service_client.go [new file with mode: 0644]
certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_factory.go [new file with mode: 0644]
certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_factory_test.go [new file with mode: 0644]
certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_test.go [new file with mode: 0644]
certServiceK8sExternalProvider/src/cmpv2controller/certificate_request_controller.go
certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner.go
certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_factory.go
certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_factory_test.go
certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_test.go
certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/expected_signed.pem [moved from certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/expected_signed.pem with 100% similarity]
certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/expected_trusted.pem [moved from certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/expected_trusted.pem with 100% similarity]
certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/test_certificate.pem [moved from certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/test_certificate.pem with 100% similarity]
certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/test_certificate_request.pem [moved from certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/test_certificate_request.pem with 100% similarity]
certServiceK8sExternalProvider/src/testdata/constants.go [new file with mode: 0644]

index 60665d7..e19e635 100644 (file)
@@ -32,17 +32,21 @@ spec:
   secretName: _sample_secret_name_
   # Common Name
   commonName: certissuer.onap.org
+  subject:
+    organizations:
+      - Linux-Foundation
+    countries:
+      - US
+    localities:
+      - San-Francisco
+    provinces:
+      - California
+    organizationalUnits:
+      - ONAP
   # DNS SAN
   dnsNames:
     - localhost
     - certissuer.onap.org
-  # IP Address SAN
-  ipAddresses:
-    - "127.0.0.1"
-  # Duration of the certificate
-  duration: 24h
-  # Renew 8 hours before the certificate expiration
-  renewBefore: 8h
   # The reference to the CMPv2 issuer
   issuerRef:
     group: certmanager.onap.org
diff --git a/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client.go b/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client.go
new file mode 100644 (file)
index 0000000..870a3ed
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * ============LICENSE_START=======================================================
+ * oom-certservice-k8s-external-provider
+ * ================================================================================
+ * Copyright (C) 2020 Nokia. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package certserviceclient
+
+import (
+       "encoding/base64"
+       "encoding/json"
+       "net/http"
+)
+
+const (
+       CsrHeaderName = "CSR"
+       PkHeaderName = "PK"
+)
+
+type CertServiceClient interface {
+       GetCertificates(csr []byte, key []byte) (*CertificatesResponse, error)
+}
+
+type CertServiceClientImpl struct {
+       certificationUrl string
+       httpClient       HTTPClient
+}
+
+type HTTPClient interface {
+       Do(req *http.Request) (*http.Response, error)
+}
+
+type CertificatesResponse struct {
+       CertificateChain    []string `json:"certificateChain"`
+       TrustedCertificates []string `json:"trustedCertificates"`
+}
+
+func (client *CertServiceClientImpl) GetCertificates(csr []byte, key []byte) (*CertificatesResponse, error) {
+
+       request, err := http.NewRequest("GET", client.certificationUrl, nil)
+       if err != nil {
+               return nil, err
+       }
+
+       request.Header.Add(CsrHeaderName, base64.StdEncoding.EncodeToString(csr))
+       request.Header.Add(PkHeaderName, base64.StdEncoding.EncodeToString(key))
+       response, err := client.httpClient.Do(request)
+       if err != nil {
+               return nil, err
+       }
+
+       var certificatesResponse CertificatesResponse
+       err = json.NewDecoder(response.Body).Decode(&certificatesResponse)
+       if err != nil {
+               return nil, err
+       }
+
+       return &certificatesResponse, err
+}
diff --git a/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_factory.go b/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_factory.go
new file mode 100644 (file)
index 0000000..198f229
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * ============LICENSE_START=======================================================
+ * oom-certservice-k8s-external-provider
+ * ================================================================================
+ * Copyright (C) 2020 Nokia. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package certserviceclient
+
+import (
+       "crypto/tls"
+       "crypto/x509"
+       "fmt"
+       "net/http"
+       "net/url"
+       "path"
+)
+
+func CreateCertServiceClient(baseUrl string, caName string, keyPemBase64 []byte, certPemBase64 []byte, cacertPemBase64 []byte) (*CertServiceClientImpl, error) {
+       cert, err := tls.X509KeyPair(certPemBase64, keyPemBase64)
+       if err != nil {
+               return nil, err
+       }
+       x509.NewCertPool()
+       caCertPool := x509.NewCertPool()
+       ok := caCertPool.AppendCertsFromPEM(cacertPemBase64)
+       if !ok {
+               return nil, fmt.Errorf("couldn't certs from cacert")
+       }
+       httpClient := &http.Client{
+               Transport: &http.Transport{
+                       TLSClientConfig: &tls.Config{
+                               RootCAs:      caCertPool,
+                               Certificates: []tls.Certificate{cert},
+                       },
+               },
+       }
+       certificationUrl, err := parseUrl(baseUrl, caName)
+       if err != nil {
+               return nil, err
+       }
+       client := CertServiceClientImpl{
+               certificationUrl: certificationUrl.String(),
+               httpClient:       httpClient,
+       }
+
+       return &client, nil
+}
+
+func parseUrl(baseUrl string, caName string) (*url.URL, error) {
+       parsedUrl, err := url.Parse(baseUrl)
+       if err != nil {
+               return nil, err
+       }
+       if caName == "" {
+               return nil, fmt.Errorf("caName cannot be empty")
+       }
+
+       parsedUrl.Path = path.Join(parsedUrl.Path, caName)
+       return parsedUrl, nil
+}
diff --git a/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_factory_test.go b/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_factory_test.go
new file mode 100644 (file)
index 0000000..50a6d79
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * ============LICENSE_START=======================================================
+ * oom-certservice-k8s-external-provider
+ * ================================================================================
+ * Copyright (C) 2020 Nokia. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package certserviceclient
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+
+       "onap.org/oom-certservice/k8s-external-provider/src/testdata"
+)
+
+const (
+       validUrl                 = "https://oom-cert-service:8443/v1/certificate/"
+       validUrl2                = "https://oom-cert-service:8443/v1/certificate"
+       invalidUrl               = "https://oom-cert  service:8443/v1/certificate"
+       caName                   = "RA"
+       expectedCertificationUrl = "https://oom-cert-service:8443/v1/certificate/RA"
+)
+
+func Test_shouldCreateCertServiceClient(t *testing.T) {
+       shouldCreateCertServiceClientWithExpectedUrl(t, expectedCertificationUrl, validUrl)
+       shouldCreateCertServiceClientWithExpectedUrl(t, expectedCertificationUrl, validUrl2)
+}
+
+func shouldCreateCertServiceClientWithExpectedUrl(t *testing.T, expectedCertificationUrl string, baseUrl string) {
+       client, err := CreateCertServiceClient(baseUrl, caName, testdata.KeyBytes, testdata.CertBytes, testdata.CacertBytes)
+
+       assert.NotNil(t, client)
+       assert.Nil(t, err)
+       assert.Equal(t, expectedCertificationUrl, client.certificationUrl)
+}
+
+func Test_shouldReturnError_whenUrlInvalid(t *testing.T) {
+       client, err := CreateCertServiceClient(invalidUrl, caName, testdata.KeyBytes, testdata.CertBytes, testdata.CacertBytes)
+
+       assert.Nil(t, client)
+       assert.Error(t, err)
+}
+
+func Test_shouldReturnError_whenCanameEmpty(t *testing.T) {
+       client, err := CreateCertServiceClient(validUrl, "", testdata.KeyBytes, testdata.CertBytes, testdata.CacertBytes)
+
+       assert.Nil(t, client)
+       assert.Error(t, err)
+}
+
+func Test_shouldReturnError_whenKeyNotMatchingCert(t *testing.T) {
+       client, err := CreateCertServiceClient(validUrl, caName, testdata.NotMatchingKeyBytes, testdata.CertBytes, testdata.CacertBytes)
+
+       assert.Nil(t, client)
+       assert.Error(t, err)
+}
+
+func Test_shouldReturnError_whenKeyInvalid(t *testing.T) {
+       //Cert used as key
+       client, err := CreateCertServiceClient(validUrl, caName, testdata.CertBytes, testdata.CertBytes, testdata.CacertBytes)
+
+       assert.Nil(t, client)
+       assert.Error(t, err)
+}
+
+func Test_shouldReturnError_whenCertInvalid(t *testing.T) {
+       //Cacert used as cert
+       client, err := CreateCertServiceClient(validUrl, caName, testdata.KeyBytes, testdata.CacertBytes, testdata.CacertBytes)
+
+       assert.Nil(t, client)
+       assert.Error(t, err)
+}
+
+func Test_shouldReturnError_whenCacertInvalid(t *testing.T) {
+       //Key used as cacert
+       client, err := CreateCertServiceClient(validUrl, caName, testdata.KeyBytes, testdata.CertBytes, testdata.KeyBytes)
+
+       assert.Nil(t, client)
+       assert.Error(t, err)
+}
diff --git a/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_test.go b/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_test.go
new file mode 100644 (file)
index 0000000..1e15d43
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * ============LICENSE_START=======================================================
+ * oom-certservice-k8s-external-provider
+ * ================================================================================
+ * Copyright (C) 2020 Nokia. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package certserviceclient
+
+import (
+       "bytes"
+       "fmt"
+       "io/ioutil"
+       "net/http"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+
+       "onap.org/oom-certservice/k8s-external-provider/src/testdata"
+)
+
+const (
+       certificationUrl = "https://oom-cert-service:8443/v1/certificate/RA"
+)
+
+
+func Test_shouldParseCertificateResponseCorrectly(t *testing.T) {
+       responseJson := `{"certificateChain": ["cert-0", "cert-1"], "trustedCertificates": ["trusted-cert-0", "trusted-cert-1"]}`
+       responseJsonReader := ioutil.NopCloser(bytes.NewReader([]byte(responseJson)))
+       client := CertServiceClientImpl{
+               certificationUrl: certificationUrl,
+               httpClient:       &httpClientMock{
+                       DoFunc: func(req *http.Request) (response *http.Response, e error) {
+                               mockedResponse := &http.Response{
+                                       Body: responseJsonReader,
+                               }
+                               return mockedResponse, nil
+                       },
+               },
+       }
+       response, _ := client.GetCertificates(testdata.CsrBytes, testdata.PkBytes)
+       assert.ElementsMatch(t, []string{"cert-0", "cert-1"}, response.CertificateChain)
+       assert.ElementsMatch(t, []string{"trusted-cert-0", "trusted-cert-1"}, response.TrustedCertificates)
+}
+
+func Test_shouldReturnError_whenResponseIsNotJson(t *testing.T) {
+       responseJson := `not a json`
+       responseJsonReader := ioutil.NopCloser(bytes.NewReader([]byte(responseJson)))
+       client := CertServiceClientImpl{
+               certificationUrl: certificationUrl,
+               httpClient:       &httpClientMock{
+                       DoFunc: func(req *http.Request) (response *http.Response, e error) {
+                               mockedResponse := &http.Response{
+                                       Body: responseJsonReader,
+                               }
+                               return mockedResponse, nil
+                       },
+               },
+       }
+       response, err := client.GetCertificates(testdata.CsrBytes, testdata.PkBytes)
+
+       assert.Nil(t, response)
+       assert.Error(t, err)
+}
+
+func Test_shouldReturnError_whenHttpClientReturnsError(t *testing.T) {
+       client := CertServiceClientImpl{
+               certificationUrl: certificationUrl,
+               httpClient:       &httpClientMock{
+                       DoFunc: func(req *http.Request) (response *http.Response, err error) {
+                               return nil, fmt.Errorf("mock error")
+                       },
+               },
+       }
+       response, err := client.GetCertificates(testdata.CsrBytes, testdata.PkBytes)
+
+       assert.Nil(t, response)
+       assert.Error(t, err)
+}
+
+
+type httpClientMock struct {
+       DoFunc func(*http.Request) (*http.Response, error)
+}
+
+func (client httpClientMock) Do(req *http.Request) (*http.Response, error) {
+       return client.DoFunc(req)
+}
index 54b4b10..d526bbc 100644 (file)
@@ -44,6 +44,11 @@ import (
        provisioners "onap.org/oom-certservice/k8s-external-provider/src/cmpv2provisioner"
 )
 
+const (
+       privateKeySecretNameAnnotation = "cert-manager.io/private-key-secret-name"
+       privateKeySecretKey = "tls.key"
+)
+
 // CertificateRequestController reconciles a CMPv2Issuer object.
 type CertificateRequestController struct {
        client.Client
@@ -104,14 +109,27 @@ func (controller *CertificateRequestController) Reconcile(k8sRequest ctrl.Reques
                return ctrl.Result{}, err
        }
 
-       // 7. Sign CertificateRequest
-       signedPEM, trustedCAs, err := provisioner.Sign(ctx, certificateRequest)
+       // 7. Get private key matching CertificateRequest
+       privateKeySecretName := certificateRequest.ObjectMeta.Annotations[privateKeySecretNameAnnotation]
+       privateKeySecretNamespaceName := types.NamespacedName{
+               Namespace: k8sRequest.Namespace,
+               Name:      privateKeySecretName,
+       }
+       var privateKeySecret core.Secret
+       if err := controller.Client.Get(ctx, privateKeySecretNamespaceName, &privateKeySecret); err != nil {
+               controller.handleErrorGettingPrivateKey(ctx, log, err, certificateRequest, privateKeySecretNamespaceName)
+               return ctrl.Result{}, err
+       }
+       privateKeyBytes := privateKeySecret.Data[privateKeySecretKey]
+
+       // 8. Sign CertificateRequest
+       signedPEM, trustedCAs, err := provisioner.Sign(ctx, certificateRequest, privateKeyBytes)
        if err != nil {
                controller.handleErrorFailedToSignCertificate(ctx, log, err, certificateRequest)
                return ctrl.Result{}, err
        }
 
-       // 8. Store signed certificates in CertificateRequest
+       // 9. Store signed certificates in CertificateRequest
        certificateRequest.Status.Certificate = signedPEM
        certificateRequest.Status.CA = trustedCAs
        if err := controller.updateCertificateRequestWithSignedCerficates(ctx, certificateRequest); err != nil {
@@ -188,6 +206,11 @@ func (controller *CertificateRequestController) handleErrorGettingCMPv2Issuer(ct
        _ = controller.setStatus(ctx, certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, "Failed to retrieve CMPv2Issuer resource %s: %v", issuerNamespaceName, err)
 }
 
+func (controller *CertificateRequestController) handleErrorGettingPrivateKey(ctx context.Context, log logr.Logger, err error, certificateRequest *cmapi.CertificateRequest, pkSecretNamespacedName types.NamespacedName) {
+       log.Error(err, "Failed to retrieve private key secret for certificate request", "namespace", pkSecretNamespacedName.Namespace, "name", pkSecretNamespacedName.Name)
+       _ = controller.setStatus(ctx, certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, "Failed to retrieve private key secret: %v", err)
+}
+
 func (controller *CertificateRequestController) handleErrorFailedToSignCertificate(ctx context.Context, log logr.Logger, err error, certificateRequest *cmapi.CertificateRequest) {
        log.Error(err, "Failed to sign certificate request")
        _ = controller.setStatus(ctx, certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, "Failed to sign certificate request: %v", err)
index e48b527..67d719c 100644 (file)
@@ -38,33 +38,29 @@ import (
        "k8s.io/apimachinery/pkg/types"
        ctrl "sigs.k8s.io/controller-runtime"
 
+       "onap.org/oom-certservice/k8s-external-provider/src/certserviceclient"
        "onap.org/oom-certservice/k8s-external-provider/src/cmpv2api"
 )
 
 var collection = new(sync.Map)
 
 type CertServiceCA struct {
-       name   string
-       url    string
-       caName string
-       key    []byte
-       cert   []byte
-       cacert []byte
+       name              string
+       url               string
+       caName            string
+       certServiceClient certserviceclient.CertServiceClient
 }
 
-func New(cmpv2Issuer *cmpv2api.CMPv2Issuer, key []byte, cert []byte, cacert []byte) (*CertServiceCA, error) {
+func New(cmpv2Issuer *cmpv2api.CMPv2Issuer, certServiceClient certserviceclient.CertServiceClient) (*CertServiceCA, error) {
 
        ca := CertServiceCA{}
        ca.name = cmpv2Issuer.Name
        ca.url = cmpv2Issuer.Spec.URL
        ca.caName = cmpv2Issuer.Spec.CaName
-       ca.key = key
-       ca.cert = cert
-       ca.cacert = cacert
+       ca.certServiceClient = certServiceClient
 
        log := ctrl.Log.WithName("cmpv2-provisioner")
-       log.Info("Configuring CA: ", "name", ca.name, "url", ca.url, "caName", ca.caName, "key", ca.key,
-               "cert", ca.cert, "cacert", ca.cacert)
+       log.Info("Configuring CA: ", "name", ca.name, "url", ca.url, "caName", ca.caName)
 
        return &ca, nil
 }
@@ -82,22 +78,27 @@ func Store(namespacedName types.NamespacedName, provisioner *CertServiceCA) {
        collection.Store(namespacedName, provisioner)
 }
 
-func (ca *CertServiceCA) Sign(ctx context.Context, certificateRequest *certmanager.CertificateRequest) ([]byte, []byte, error) {
+func (ca *CertServiceCA) Sign(ctx context.Context, certificateRequest *certmanager.CertificateRequest, privateKeyBytes []byte) ([]byte, []byte, error) {
        log := ctrl.Log.WithName("certservice-provisioner")
        log.Info("Signing certificate: ", "cert-name", certificateRequest.Name)
 
-       key, _ := base64.RawStdEncoding.DecodeString(string(ca.key))
-       log.Info("CA: ", "name", ca.name, "url", ca.url, "key", key)
+       log.Info("CA: ", "name", ca.name, "url", ca.url)
 
-       crPEM := certificateRequest.Spec.Request
-       csrBase64 := crPEM
-       log.Info("Csr PEM: ", "bytes", csrBase64)
+       csrBytes := certificateRequest.Spec.Request
+       log.Info("Csr PEM: ", "bytes", csrBytes)
 
-       csr, err := decodeCSR(crPEM)
+       csr, err := decodeCSR(csrBytes)
        if err != nil {
                return nil, nil, err
        }
 
+       response, err := ca.certServiceClient.GetCertificates(csrBytes, privateKeyBytes)
+       if err != nil {
+               return nil, nil, err
+       }
+       log.Info("Certificate Chain", "cert-chain", response.CertificateChain)
+       log.Info("Trusted Certificates", "trust-certs", response.TrustedCertificates)
+
        cert := x509.Certificate{}
        cert.Raw = csr.Raw
 
index 4a3898e..125c1bc 100644 (file)
@@ -25,24 +25,31 @@ import (
 
        v1 "k8s.io/api/core/v1"
 
+       "onap.org/oom-certservice/k8s-external-provider/src/certserviceclient"
        "onap.org/oom-certservice/k8s-external-provider/src/cmpv2api"
 )
 
 func CreateProvisioner(issuer *cmpv2api.CMPv2Issuer, secret v1.Secret) (*CertServiceCA, error) {
        secretKeys := issuer.Spec.CertSecretRef
-       key, err := readValueFromSecret(secret, secretKeys.KeyRef)
+       keyBase64, err := readValueFromSecret(secret, secretKeys.KeyRef)
        if err != nil {
                return nil, err
        }
-       cert, err := readValueFromSecret(secret, secretKeys.CertRef)
+       certBase64, err := readValueFromSecret(secret, secretKeys.CertRef)
        if err != nil {
                return nil, err
        }
-       cacert, err := readValueFromSecret(secret, secretKeys.CacertRef)
+       cacertBase64, err := readValueFromSecret(secret, secretKeys.CacertRef)
        if err != nil {
                return nil, err
        }
-       return New(issuer, key, cert, cacert)
+
+       certServiceClient, err := certserviceclient.CreateCertServiceClient(issuer.Spec.URL, issuer.Spec.CaName, keyBase64, certBase64, cacertBase64)
+       if err != nil {
+               return nil, err
+       }
+
+       return New(issuer, certServiceClient)
 }
 
 func readValueFromSecret(secret v1.Secret, secretKey string) ([]byte, error) {
index 6ef3309..1e215d3 100644 (file)
@@ -21,6 +21,7 @@
 package cmpv2provisioner
 
 import (
+       "encoding/base64"
        "fmt"
        "testing"
 
@@ -28,6 +29,7 @@ import (
        v1 "k8s.io/api/core/v1"
 
        "onap.org/oom-certservice/k8s-external-provider/src/cmpv2api"
+       "onap.org/oom-certservice/k8s-external-provider/src/testdata"
 )
 
 const (
@@ -39,12 +41,6 @@ const (
        cacertSecretKey = "cacert.pem"
 )
 
-var (
-       keySecretValue    = []byte("keyData")
-       certSecretValue   = []byte("certData")
-       cacertSecretValue = []byte("cacertData")
-)
-
 func Test_shouldCreateProvisioner(t *testing.T) {
        issuer, secret := getValidIssuerAndSecret()
 
@@ -53,9 +49,6 @@ func Test_shouldCreateProvisioner(t *testing.T) {
        assert.NotNil(t, provisioner)
        assert.Equal(t, url, provisioner.url)
        assert.Equal(t, caName, provisioner.caName)
-       assert.Equal(t, keySecretValue, provisioner.key)
-       assert.Equal(t, certSecretValue, provisioner.cert)
-       assert.Equal(t, cacertSecretValue, provisioner.cacert)
 }
 
 func Test_shouldReturnError_whenSecretMissingKeyRef(t *testing.T) {
@@ -94,6 +87,18 @@ func Test_shouldReturnError_whenSecretMissingCacertRef(t *testing.T) {
        }
 }
 
+
+func Test_shouldReturnError_whenCreationOfCertServiceClientReturnsError(t *testing.T) {
+       issuer, secret := getValidIssuerAndSecret()
+       invalidKeySecretValue, _    := base64.StdEncoding.DecodeString("")
+       secret.Data[keySecretKey] = invalidKeySecretValue
+
+       provisioner, err := CreateProvisioner(&issuer, secret)
+
+       assert.Nil(t, provisioner)
+       assert.Error(t, err)
+}
+
 func getValidIssuerAndSecret() (cmpv2api.CMPv2Issuer, v1.Secret) {
        issuer := cmpv2api.CMPv2Issuer{
                Spec: cmpv2api.CMPv2IssuerSpec{
@@ -110,9 +115,9 @@ func getValidIssuerAndSecret() (cmpv2api.CMPv2Issuer, v1.Secret) {
        secret := v1.Secret{
 
                Data: map[string][]byte{
-                       keySecretKey:    keySecretValue,
-                       certSecretKey:   certSecretValue,
-                       cacertSecretKey: cacertSecretValue,
+                       keySecretKey:    testdata.KeyBytes,
+                       certSecretKey:   testdata.CertBytes,
+                       cacertSecretKey: testdata.CacertBytes,
                },
        }
        secret.Name = secretName
index f3ab5cb..39e399b 100644 (file)
@@ -33,31 +33,26 @@ import (
        apimach "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/types"
 
+       "onap.org/oom-certservice/k8s-external-provider/src/certserviceclient"
        "onap.org/oom-certservice/k8s-external-provider/src/cmpv2api"
 )
 
 const ISSUER_NAME = "cmpv2-issuer"
 const ISSUER_URL = "issuer/url"
-const KEY = "onapwro-key"
-const CERT = "onapwro-cert"
-const CACERT = "onapwro-cacert"
 const ISSUER_NAMESPACE = "onap"
 
 func Test_shouldCreateCorrectCertServiceCA(t *testing.T) {
-       issuer, key, cert, cacert := createIssuerAndCerts(ISSUER_NAME, ISSUER_URL, KEY, CERT, CACERT)
-       provisioner, err := New(&issuer, key, cert, cacert)
+       issuer := createIssuerAndCerts(ISSUER_NAME, ISSUER_URL)
+       provisioner, err := New(&issuer, &certServiceClientMock{})
 
        assert.Nil(t, err)
-       assert.Equal(t, string(provisioner.key), string(key), "Unexpected provisioner key.")
-       assert.Equal(t, string(provisioner.cert), string(cert), "Unexpected provisioner cert.")
-       assert.Equal(t, string(provisioner.cacert), string(cacert), "Unexpected provisioner cacert.")
        assert.Equal(t, provisioner.name, issuer.Name, "Unexpected provisioner name.")
        assert.Equal(t, provisioner.url, issuer.Spec.URL, "Unexpected provisioner url.")
 }
 
 func Test_shouldSuccessfullyLoadPreviouslyStoredProvisioner(t *testing.T) {
-       issuer, key, cert, cacert := createIssuerAndCerts(ISSUER_NAME, ISSUER_URL, KEY, CERT, CACERT)
-       provisioner, err := New(&issuer, key, cert, cacert)
+       issuer := createIssuerAndCerts(ISSUER_NAME, ISSUER_URL)
+       provisioner, err := New(&issuer, &certServiceClientMock{})
 
        assert.Nil(t, err)
 
@@ -67,19 +62,24 @@ func Test_shouldSuccessfullyLoadPreviouslyStoredProvisioner(t *testing.T) {
        provisioner, ok := Load(issuerNamespaceName)
 
        verifyThatConditionIsTrue(ok, "Provisioner could not be loaded.", t)
-       assert.Equal(t, string(provisioner.key), string(key), "Unexpected provisioner key.")
-       assert.Equal(t, string(provisioner.cert), string(cert), "Unexpected provisioner cert.")
-       assert.Equal(t, string(provisioner.cacert), string(cacert), "Unexpected provisioner cacert.")
        assert.Equal(t, provisioner.name, issuer.Name, "Unexpected provisioner name.")
        assert.Equal(t, provisioner.url, issuer.Spec.URL, "Unexpected provisioner url.")
 }
 
 func Test_shouldReturnCorrectSignedPemsWhenParametersAreCorrect(t *testing.T) {
-       const EXPECTED_SIGNED_FILENAME = "test_resources/expected_signed.pem"
-       const EXPECTED_TRUSTED_FILENAME = "test_resources/expected_trusted.pem"
-
-       issuer, key, cert, cacert := createIssuerAndCerts(ISSUER_NAME, ISSUER_URL, KEY, CERT, CACERT)
-       provisioner, err := New(&issuer, key, cert, cacert)
+       const EXPECTED_SIGNED_FILENAME = "testdata/expected_signed.pem"
+       const EXPECTED_TRUSTED_FILENAME = "testdata/expected_trusted.pem"
+
+       issuer := createIssuerAndCerts(ISSUER_NAME, ISSUER_URL)
+       provisioner, err := New(&issuer, &certServiceClientMock{
+               getCertificatesFunc: func(csr []byte, pk []byte) (response *certserviceclient.CertificatesResponse, e error) {
+                       mockResponse:= &certserviceclient.CertificatesResponse{
+                               CertificateChain:    []string{"cert-0", "cert-1"},
+                               TrustedCertificates: []string{"trusted-cert-0", "trusted-cert-1"},
+                       } //TODO: mock real certServiceClient response
+                       return mockResponse, nil
+               },
+       })
 
        issuerNamespaceName := createIssuerNamespaceName(ISSUER_NAMESPACE, ISSUER_NAME)
        Store(issuerNamespaceName, provisioner)
@@ -91,7 +91,7 @@ func Test_shouldReturnCorrectSignedPemsWhenParametersAreCorrect(t *testing.T) {
        ctx := context.Background()
        request := createCertificateRequest()
 
-       signedPEM, trustedCAs, err := provisioner.Sign(ctx, request)
+       signedPEM, trustedCAs, err := provisioner.Sign(ctx, request, nil)
 
        assert.Nil(t, err)
 
@@ -112,11 +112,11 @@ func createIssuerNamespaceName(namespace string, name string) types.NamespacedNa
        }
 }
 
-func createIssuerAndCerts(name string, url string, key string, cert string, cacert string) (cmpv2api.CMPv2Issuer, []byte, []byte, []byte) {
+func createIssuerAndCerts(name string, url string) cmpv2api.CMPv2Issuer {
        issuer := cmpv2api.CMPv2Issuer{}
        issuer.Name = name
        issuer.Spec.URL = url
-       return issuer, []byte(key), []byte(cert), []byte(cacert)
+       return issuer
 }
 
 func readFile(filename string) []byte {
@@ -133,8 +133,8 @@ func createCertificateRequest() *cmapi.CertificateRequest {
        const ISSUER_GROUP = "certmanager.onap.org"
        const CONDITION_TYPE = "Ready"
 
-       const SPEC_REQUEST_FILENAME = "test_resources/test_certificate_request.pem"
-       const STATUS_CERTIFICATE_FILENAME = "test_resources/test_certificate.pem"
+       const SPEC_REQUEST_FILENAME = "testdata/test_certificate_request.pem"
+       const STATUS_CERTIFICATE_FILENAME = "testdata/test_certificate.pem"
 
        duration := new(apimach.Duration)
        d, _ := time.ParseDuration(CERTIFICATE_DURATION)
@@ -159,3 +159,11 @@ func createCertificateRequest() *cmapi.CertificateRequest {
 func areSlicesEqual(slice1 []byte, slice2 []byte) bool {
        return bytes.Compare(slice1, slice2) == 0
 }
+
+type certServiceClientMock struct {
+       getCertificatesFunc func(csr []byte, key []byte) (*certserviceclient.CertificatesResponse, error)
+}
+
+func (client *certServiceClientMock) GetCertificates(csr []byte, key []byte) (*certserviceclient.CertificatesResponse, error) {
+       return client.getCertificatesFunc(csr, key)
+}
diff --git a/certServiceK8sExternalProvider/src/testdata/constants.go b/certServiceK8sExternalProvider/src/testdata/constants.go
new file mode 100644 (file)
index 0000000..d2097ba
--- /dev/null
@@ -0,0 +1,12 @@
+package testdata
+
+import "encoding/base64"
+
+var (
+       KeyBytes, _            = base64.StdEncoding.DecodeString("QmFnIEF0dHJpYnV0ZXMKICAgIGZyaWVuZGx5TmFtZTogb29tLWNlcnQtc2VydmljZQogICAgbG9jYWxLZXlJRDogNTQgNjkgNkQgNjUgMjAgMzEgMzYgMzAgMzIgMzggMzMgMzkgMzIgMzIgMzAgMzcgMzkgMzUgCktleSBBdHRyaWJ1dGVzOiA8Tm8gQXR0cmlidXRlcz4KLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ3IwcWpSamNIb3lNY20KREJYanRlcm45V0pYWHlDQUlOREJ6SFV0MHR6WFFHbmlpSU9URXhldGdYdUl2KytoNEVrZ25SalVKNmVzSktFKwo3VHZnd21VS1hqc3ljY3pNWUhHMUE5bk5EN3BZcXAzY0dJZUp0ek9xV3hDVTh1MFhtMnRBVGI5Mm5VNHd0elpXCnhpbEJxa0pySmo3aThPbnVlaE0rR2tWUmZMUCtxUWJUUTVueE53dytlMlpMTWcyb0g3TDMwbTdhbTRxZ1hFL1IKdEdVcjU0a3dXZlVPQTN2MVlzd1daRjFZblhwRDRvRmdndGJZWWRVU1c5QnBTVzZITkRGeFFoMmtpQVJybU9rWgpXTGNiYzd6RnpSN0dIRzJlclhCODhZN3A3dFV5ZW5PUll6RkhnM0NEdGg3MUVUbCtUZjZITDZienBqc2E2UmNJClVyS1hxS3RuQWdNQkFBRUNnZ0VBTm9XMlJDaXphMmFxcXd3U3RoczMyenNtWllzdUNQcGd3OTVaSUoxVXJva20KRUZnNVNDWTYwVGZSTjJlUVp0R0E0dlIydUh1TTNUY1NZNkZyNnJwRXpiRnhIMlMxRS9WV241WUZPdWpPdk93SApBNXhWQmdJNFJzcDJ6SXo1WnhCT1RDMWZvQWZ5azhyUFYyR3lIY0FsSzFNTGlYL2crMmVKUzUrU2QzVVd1S3ZxCm1NQnJzeUhYeTZjR2p3dmlsdzBqYUUzOVhLNTVNRE95b1pNZUo4VDRlRkJBRXlvQm85amZYWjhrbXREM05PZVQKOTJYZW0rL2dnekRaMWtTWUVxOHBkZEpDRW9VZ2lzVmZ1ZUtKdDRNek9FcUtXOXNVbUp6NjZOK0FnT2hzbDU2VQo3dUhpdDRGV08rVmJtR0JQQ2hCSzMzR1hJNTZJRDZWakQyNllrWlNCd1FLQmdRRDlqTlk2cW9yWmdXbzhucnEyCk5VNzdRVmNyOVVzd2htQlM2UE1zR01XWGYveFg3cUVaeUlIbzIxTzg4TkZ4c2lXazUwZzY4UGVTTnQ0VGRyZ3kKTG5Ra3o4MzhWTk9ZdjNlQkZFWFY2RXFFUFY2VUtlZWU4RDYyMitET2JYaUtqS0tweVlqaXo4NmRPcUdLeTJjeApETlZGQnRDWEViRjErcEtmYWRtOU1YeS9PUUtCZ1FDdGU2c0Q4T0NOdyswSzMzK0xhUGg1Tlkyb0djNVZyNm5yCk4xNnlVV0lOVEFGVk1KZkFBUjlJNFpDR2V5RUw1UVNYOFpsRjlSOWNlVW9TaWJSMmdoOFV5dDQrZ0ZDU3JBSW8KUTBrWkhJeG9BYnp5MG9BanJINGtwNVd2QURld1ZCQ25aSmxXeGFjcUZMR2N0WTZRYW1PalpWVXVwSlVOQ2h6UwpwM2FMdWpZZm53S0JnRmpXNXgxSk1qdUIxK3FEcDJJK2pYMEY2UGhUQzFSbVVRdmI2WkJ5NFpEeTNFVW5MTFZ2CkJ1M0RJN1VaSUJuWlZNMVI2SUlXZW5oMTF4dzB4ZGQzWldTY2wwMHBuN1p1cC8zSFQ2emlwbkZ0VzExSXpZcG8KSFdGTzY1Y0l6bXFsV2oxcGl4Z0Z2aHhqTmNUKy9obzJwK2QydXRHajltMGpMZ3JET1BMTWl5d3BBb0dBRTU0ZgplYU9ja1F0dDI4UG9WV2g2YUtLQXNWaXh0NGpVeXkrSXV0dHZIaGZSTXNQNjlSQnJiRDl0cTNkekJqaFFxNm43CmJpakk4aGtaSWoyR05ieURMVU8vbkF2QWtNVjR2UHJXNGtzVEtaUEF2U2pHcXNJUHhhOVp3dDlnYk1VazJQa00KU2Y2eDU1Vk5mRzRmZi84MzR6dExSYW9BM09lZTJNZHRKV0hhU3ZzQ2dZRUF5b3dia2ZwdGFNSU9vbHg5Y2tmSwpEU0VNOEJtNUR1SVpqM1ZCWEdRZUtmMncvWHBXekR5SWRDdy9ZODBFNWRSNWlMZEhWS1BvOVJqeTY5bmpLeTNrCnJta2pzczMxa2dLaTFYR0FCMytTN2xmUE1sQnFCayt5WHVET1pWK3ZzZFFWR05xM1gwUFVkR09wV0g2VUtzTm8Kb3NOR042SHh0Z0NFaDUxdlNvT2tBY0U9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K")
+       NotMatchingKeyBytes, _ = base64.StdEncoding.DecodeString("QmFnIEF0dHJpYnV0ZXMKICAgIGZyaWVuZGx5TmFtZTogb29tLWNlcnQtc2VydmljZQogICAgbG9jYWxLZXlJRDogNTQgNjkgNkQgNjUgMjAgMzEgMzYgMzAgMzMgMzMgMzUgMzEgMzIgMzEgMzMgMzcgMzEgMzEgCktleSBBdHRyaWJ1dGVzOiA8Tm8gQXR0cmlidXRlcz4KLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQ29QeTljcE81a0NZUzQKSHpQY25UMVJkTkNYcis1Zk54VzlYUFEyUUNUYUJiWk9PeTJYbEVJdWVpWW5KUTQwb0VQelZYZWdxSXkwS1QyMwo3ZHJhSXlENkIyK282TmlsYkJLdlRzbnFkYytwRXdtRC96cXRTSUlhcEszNWl2SDNmcUZoZ0JZRWI2RkJSZzE0CkFDRlJwUWlkcVRzRHQyVFZmTEljU1ZIbElzRUU2UmRNcFgxN2ZQNVFjNUc4MCtONHp1b0tIQUV0V2xpWTBhZDYKT0k1TnlnMGNkK1VwZ0p6NFdPQVkwajV4a2Q0TWU3TW00TmlIMmZFa2pwdGdCbEVrTGo1bkVuQmVQRzhPaFBlYgphOVc3T1BXOTRodFc3RlN3QjMxWE1CMmhyZlJUNUZLanNhakgzSllRZkQrajdQc1d0QVhlYXc1dW93L1hVSEFnCnhwZHpxREtsQWdNQkFBRUNnZ0VBTnpNejJOT01HMm84RHlTOW9UNDlwZ0lMaGhrRXZseVdWMkN1QnphWVlLZkwKSG5Pd0M3RnEzVkRhUDdHYXJZMS9mZENteFNGSHVMeG5NWGRxZkhOV0dISmtJaWp6RzZNUHBsRFZVb1dBc2xvNgpud0lZUUU0djBZb0NRb05oeSsvU0J1azlVQXRIL3VCNi9zb2NKR3RmSGtzdHY1Um1FdDBzbXJjN2xWQUh3QklvCkcxOXlJeXdOai9nWS9LQmpuWElZaXgrS0kvR1AyUFNzZGJOTU5DNUtqMjc0NW1WNGRYaHJCNEhDWS9mMUcxWnIKa2gzRzZYeGF4ZEFJNm1pOFdIcXVFc1VzRkJKWFJLOTNNYkF1SU5GQllGQU15NitLWDY2N0lNdEVaQ3Yza2lUZQpmcjRDTmFHUDIwYk1pK0tYK2wzamdQMVBabDdhTVJWZkdPYlRXZk5hYVFLQmdRRHdSZHFiVFBXeEsySFh4bURVCnRDanp0SFRrWTZjNncxV3JpWlgxejVlNzVnRWtVVlYyZ3BnS1hXUUErbGVHVGFReUhGQ1NybmJ3TzVCSlF3clYKamEzWU1tWmJVeGVIczhLNHF1ZDlVeHFzciszaVdBdmJ6WGRaV1ZrdXFzS3ZReVFteGhMbkNTak5kUFRwY2t2ZApGWmI3REdNVGg3UnowT1gzSUdHbmxRWlNld0tCZ1FDelFtd2tkMTNKNndVd3A5U2NpNU15MzVIREdHQ1FVSW1FCkkzLzVlLy8raEpKckowcGhqS3hzR3FzT2R2QWg4SGlhZlZnS2hPTTZDRkh3WEZsN2hwdGZFWnRDMFpZUW9jR3oKR203ZWpOZmxROEJsSlU0VHNjSjREWHFzTS8zcTNZQ29aOTEzTWRXeW1ITDYwZkN5bWhyKytiMU5SVXVFSklUbQp5MUdmWm9xVlh3S0JnUUNGQTViZTdMOERZdW5hbzFjTnllTDE3NHZhdUJSbklxWTF2WWhJT2JGZGN4cGt1YVlmClluSzdJakp2bkNlQ0VVOTU4bFRrcnpMbkVZSnlIR3hPQnc0YnB1TWxZWjJnSVhNRitvOEd3VS8rRTdNVTMvdEkKcHJtUXZEYno3OUt4WmZFSWloVUMwLzVEcDZEQjcrVnhzamNRS2k1YkxJQklzWGZ0MUg1YllOUE1Ld0tCZ0NPQQo0RkpJb2ZhLzZZTTlla3FYQ2t4bEkwVjBxb3RxcUJIWHhoenZoa0F1bWFGSFl0LzNNSjhvbVFDQVpnY1N6WkFyCml5aHFNV2JwQnZHSUdPRHlSQXVNUFNmNndySUFsNUNWaDZma1ZVNC9JUGZuYjVOTy9ha3hZajZBL2FWcXdYU1oKUGEvQjJ3VklWRjJBL3g4Z2pvQTVqbGlqaE5Wak5qOVB2WmJJaEdJWkFvR0JBTWZiOXlvbGtmc3FmZXJ4bUJJYQp0NFZiVXFRNWRiMUp5ZFJkblduNWxGY1lmVzhScG5lOUtLQkFKeHZ5ME1laThOMHVidHl3Z0FDQXAzYyt2VUY1CjFYNWZIdzdPbUErclBjR0hRZENUeURVUitWKzM0L0RPRUJKencreXRLZVRUWHZrcXR3blBmWVQ1cVF4Uk93N20KMFZmWVQ2bXFtaXVySUU4b2VMUDR5dGp6Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K")
+       CertBytes, _           = base64.StdEncoding.DecodeString("QmFnIEF0dHJpYnV0ZXMKICAgIGZyaWVuZGx5TmFtZTogb29tLWNlcnQtc2VydmljZQogICAgbG9jYWxLZXlJRDogNTQgNjkgNkQgNjUgMjAgMzEgMzYgMzAgMzIgMzggMzMgMzkgMzIgMzIgMzAgMzcgMzkgMzUgCnN1YmplY3Q9QyA9IFVTLCBTVCA9IENhbGlmb3JuaWEsIEwgPSBTYW4tRnJhbmNpc2NvLCBPID0gTGludXgtRm91bmRhdGlvbiwgT1UgPSBPTkFQLCBDTiA9IG9uYXAub3JnCgppc3N1ZXI9QyA9IFVTLCBTVCA9IENhbGlmb3JuaWEsIEwgPSBTYW4tRnJhbmNpc2NvLCBPID0gTGludXgtRm91bmRhdGlvbiwgT1UgPSBPTkFQLCBDTiA9IG9uYXAub3JnCgotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRkN6Q0NBdk9nQXdJQkFnSUVNOHQ3L2pBTkJna3Foa2lHOXcwQkFRd0ZBREIzTVFzd0NRWURWUVFHRXdKVgpVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTlUyRnVMVVp5WVc1amFYTmpiekVaCk1CY0dBMVVFQ2hNUVRHbHVkWGd0Um05MWJtUmhkR2x2YmpFTk1Bc0dBMVVFQ3hNRVQwNUJVREVSTUE4R0ExVUUKQXhNSWIyNWhjQzV2Y21jd0hoY05NakF4TURFMk1Ea3dOalU1V2hjTk1qRXhNREUyTURrd05qVTVXakIzTVFzdwpDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTlUyRnVMVVp5CllXNWphWE5qYnpFWk1CY0dBMVVFQ2hNUVRHbHVkWGd0Um05MWJtUmhkR2x2YmpFTk1Bc0dBMVVFQ3hNRVQwNUIKVURFUk1BOEdBMVVFQXhNSWIyNWhjQzV2Y21jd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFSwpBb0lCQVFDcjBxalJqY0hveU1jbURCWGp0ZXJuOVdKWFh5Q0FJTkRCekhVdDB0elhRR25paUlPVEV4ZXRnWHVJCnYrK2g0RWtnblJqVUo2ZXNKS0UrN1R2Z3dtVUtYanN5Y2N6TVlIRzFBOW5ORDdwWXFwM2NHSWVKdHpPcVd4Q1UKOHUwWG0ydEFUYjkyblU0d3R6Wld4aWxCcWtKckpqN2k4T251ZWhNK0drVlJmTFArcVFiVFE1bnhOd3crZTJaTApNZzJvSDdMMzBtN2FtNHFnWEUvUnRHVXI1NGt3V2ZVT0EzdjFZc3dXWkYxWW5YcEQ0b0ZnZ3RiWVlkVVNXOUJwClNXNkhOREZ4UWgya2lBUnJtT2taV0xjYmM3ekZ6UjdHSEcyZXJYQjg4WTdwN3RVeWVuT1JZekZIZzNDRHRoNzEKRVRsK1RmNkhMNmJ6cGpzYTZSY0lVcktYcUt0bkFnTUJBQUdqZ1o0d2dac3dIUVlEVlIwT0JCWUVGRFowUi95TgpKTk1WbTRQQ0doNWNnV1NHVkQxOU1Da0dBMVVkRVFFQi93UWZNQjJDRUc5dmJTMWpaWEowTFhObGNuWnBZMldDCkNXeHZZMkZzYUc5emREQVBCZ05WSFJNRUNEQUdBUUgvQWdFQU1COEdBMVVkSXdRWU1CYUFGR1FORGR2aWpRSG8KUzZtTy9nakplZ1h0MC9jc01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFOQmdrcQpoa2lHOXcwQkFRd0ZBQU9DQWdFQU9aQ1dZMXQvUFpGRWN5Y3h2T09jWW9qb0ZlcVZhdE01MHVxNVJuS2c4UG1RCkFMTGN1SGVIVVIyS2RIZ0ZYajRBeWt6V1JDU1pwdjZsekZ3WStpVUUxaWF2MENHcWR5Mml6Vkx2ampvUDFoRHQKQXRVQjg1VnBrblJwdlFvYldwN0s2TUhOMXRlUWlnZ0hBNjhqQXp1UlMwL2pUUGR6MTdXNGVkdHBrMHQ4QlRaUgpCUG9rM1VWa1JBZGEzTElFWHh0YXlaaGpuSU92WXNnZGllU2lha0VOaDFzNTRqd3B2bU5KKzk5WU91WDcrTW00ClY1UmNGZGJKRHo0UjJUaUZETEJ1d2ZXaFJUei9Ga1MrYTlvaElhc3l2MmVHM0Q1dlVGeENyZzYydWQ2c25nakgKOFRaZnpJQXFhZEluVkIxcXF2dHgrRGVZMGgxRUZUWkZiWHM2WVpLVzhuL3pjQ1FEVERSOS9yeVczNVFKdWozbgpxa1ZsTnBnRGJVRlVucXEySUl0dFNrS21vMVg2QXJRZkwva0VGNWV5YnpzdFlnZkIyZkx1ZmlobDAyNGJHY1Y3Ci9JSkNDRFEreUg5ZDJDWjJmTW1ueFppRHkyYW1BRU5nT3UxRlZLUU1UODErTjVKVFZJWFFqeEFPbnJLWGFLY2wKQXFuWHRhUEh5SUVoU0ZhL2RBRExMd2pkT2xjZ3hpMUt1UDEzTXU1c0JQWEIzVWx2ZEYyYXMzTnRjaEZOaXAzcwoxeGExcDRUejhWTFUwaXE2YmltczVlczVsVFlQd2JmZTBoTnljcHZ1NFhyTXk3MHNnYWJ3L1NMTWlHeDVUVGg0CkQ2Qkgya0VwNDUxVnUwZnFyU0xmU21aQVNaenBYZ3lSQmlUMys0c2E1QlhwaHZCd1pOeExpUzV0ZGJONHd2QT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQpCYWcgQXR0cmlidXRlcwogICAgZnJpZW5kbHlOYW1lOiBDTj1vbmFwLm9yZyxPVT1PTkFQLE89TGludXgtRm91bmRhdGlvbixMPVNhbi1GcmFuY2lzY28sU1Q9Q2FsaWZvcm5pYSxDPVVTCnN1YmplY3Q9QyA9IFVTLCBTVCA9IENhbGlmb3JuaWEsIEwgPSBTYW4tRnJhbmNpc2NvLCBPID0gTGludXgtRm91bmRhdGlvbiwgT1UgPSBPTkFQLCBDTiA9IG9uYXAub3JnCgppc3N1ZXI9QyA9IFVTLCBTVCA9IENhbGlmb3JuaWEsIEwgPSBTYW4tRnJhbmNpc2NvLCBPID0gTGludXgtRm91bmRhdGlvbiwgT1UgPSBPTkFQLCBDTiA9IG9uYXAub3JnCgotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRm5qQ0NBNGFnQXdJQkFnSUVHSEJiNkRBTkJna3Foa2lHOXcwQkFRd0ZBREIzTVFzd0NRWURWUVFHRXdKVgpVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTlUyRnVMVVp5WVc1amFYTmpiekVaCk1CY0dBMVVFQ2hNUVRHbHVkWGd0Um05MWJtUmhkR2x2YmpFTk1Bc0dBMVVFQ3hNRVQwNUJVREVSTUE4R0ExVUUKQXhNSWIyNWhjQzV2Y21jd0hoY05NakF4TURFMk1Ea3dOalV5V2hjTk16QXhNREUwTURrd05qVXlXakIzTVFzdwpDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTlUyRnVMVVp5CllXNWphWE5qYnpFWk1CY0dBMVVFQ2hNUVRHbHVkWGd0Um05MWJtUmhkR2x2YmpFTk1Bc0dBMVVFQ3hNRVQwNUIKVURFUk1BOEdBMVVFQXhNSWIyNWhjQzV2Y21jd2dnSWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUNEd0F3Z2dJSwpBb0lDQVFDREJSMDZTU1BteFVpdWc1NC9Ya1piVFN2ZTE4M2VEYityT2JBT1d1MWMzeVFIQmpCQUFFQ2E0SXVxClRaR3dOb0svdllYcjJpcnlRMDJMcHA3N3pCQ3lwVkNEckhHbDE1d0hwa0NZak5Ob3Vrb1lIaGErdkNFc3RubGgKVExCUHllclFwZGNlckhzVVRhSHBoamRrcGZMa2xGcmZGejZTQ28xa3ZJbmdoRkFFUmxqT2FOMy9pcTI3MUlBVAplcHlBVkRkVHpRK3h6TUJOUUZnRjNRVU9SaDE2NUlKNFFkOVpWY1hjakd3SUxHVjlsdzRBYUlTalZxSXBrYkxoCnB3am5BNFBtTGRadkhyN3l6VDVHTXhQWTdRVjkvN05RZmtuT1RPU1pxRlgyZHBzcVhkN21Odi9HMDgxekRiSloKYmR5VUh5QXFQbTRJN3JaKzZmckg3OFBvQ0h3QXAxbU9QNUF6VEtFWVVlbjFQQis4OGxUcWxtanhuOFZYejh2Tgo1NWZJNFlDUUg2dGxSdXdRamwxSFF5SVBEWGpoOE9KSUluNElnOWF5OUZNOUNTL0p3N0hrT2JqSW1oQU0yTW5RCkpuQ0FzT3ZYeW40amRic0dpaFpvWEYzODdPZ0x0V0NqendaWk1NQk84Rm5ibmNZZ1dlY2JuWXBFcnI3Tlp4cjEKRTNxQjNKVHNZNVRBSW1OM052ckZJeXVvdmY1NGR5ckRXUWh0bGUwY3VuZUJCUzU3SFNnWFNlRGp2Vko4V3I1MQpwZlJrZE1CbkE0Wll4SmRaamtpVzFvY1RJZXh3V2sxdVBtMC93RFVsVytwcHlzS0hUNXAyOTBOa3RSVWNCMGJ4ClA0YzkzOEl1bUNOZU5ZT1dpUENBcGVDUmlmODYwTG5oMWQzVHFHL1dQMGJUY1gySEFRSURBUUFCb3pJd01EQWQKQmdOVkhRNEVGZ1FVWkEwTjIrS05BZWhMcVk3K0NNbDZCZTNUOXl3d0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTgpCZ2txaGtpRzl3MEJBUXdGQUFPQ0FnRUFNMVFEaEM2ZEp6d0VlMHNmOHg2aXArYy9MSEFFbE9PV1g3K04vUVJ1CmlaYWNjZmdveDZhZHU0QkUrbDltVXJxS3hGQm5wb216dm9MZlNyc09ramhqMUc1dU9qSXV4QVJabktyY3dJNGoKYytXdWNTcUJCbkRxeXpMNys3RzFVbm01K3lpZmw1QUVzMngrN2Z0RnpvZ1VLV2E5M3hzUTIyYU5nRE96MCtCNgpGTCtWUEMwSlNMSDJRR1R0SEpWTUtpTEtBajFNMXJBc2l1SVNLNUtLbWs5Q0dGSmwySEFnV0s0TFQ0Y2JaUFQ4CjJCT0RMS2FXMnFGVlJKUkNSSlVCOUhaR0R6K0ZuOU14TlhtUWYwb3grK0h5Y0psZ1FNc0lET1VKajZCOGJ5Z0kKZVUwcEQ1MFJNS05DK3RuYWVITFJLTHJHQTFLYVc5a2t1MFVPL2RJTk1kbUlmaTlGS0diVUFvTGgvMmNSMWJVRgpYUXRpS2VLc0gvMEhXUS9NMmlVcEhhU1NneCt4ek54LzR3YU9MOFdkWmF0amVxVmNiUkhseS9sazZtcStXRTgxCjM4aTlyTVpNaUh3VktoYnp1d1ltUTRHTHVBZFEvUnR0clVMTTEvNEZvaE1oVXBsdnVndngrZmFqcVlmODVrTlIKb2tkdUZ4T1h0M01jMnJHZHRqby9jQ3VyMXN5S2NqWG5CK3NZbXpBYlAyWlNrRDBMbStGN2RwUDJHNDZmTC9hSwpUVExIUktxVlNHekNpekN1a1BNT2RvL0xqQnhyT01VZHVCdW5nU0VucHFHT0NHdzhuLzdkalZJbGw3ZURtaThkCmM0b25kOGN6YnFMaGNNZ2FVa2o2aFUySUR4R1ROL0hzeHh4OHE1TVN3NTdwSUF2dVQ2b0h4RUN1R0pINHRpVTkKUVM0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==")
+       CacertBytes, _         = base64.StdEncoding.DecodeString("QmFnIEF0dHJpYnV0ZXMKICAgIGZyaWVuZGx5TmFtZTogcm9vdAogICAgMi4xNi44NDAuMS4xMTM4OTQuNzQ2ODc1LjEuMTogPFVuc3VwcG9ydGVkIHRhZyA2PgpzdWJqZWN0PUMgPSBVUywgU1QgPSBDYWxpZm9ybmlhLCBMID0gU2FuLUZyYW5jaXNjbywgTyA9IExpbnV4LUZvdW5kYXRpb24sIE9VID0gT05BUCwgQ04gPSBvbmFwLm9yZwoKaXNzdWVyPUMgPSBVUywgU1QgPSBDYWxpZm9ybmlhLCBMID0gU2FuLUZyYW5jaXNjbywgTyA9IExpbnV4LUZvdW5kYXRpb24sIE9VID0gT05BUCwgQ04gPSBvbmFwLm9yZwoKLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZuakNDQTRhZ0F3SUJBZ0lFR0hCYjZEQU5CZ2txaGtpRzl3MEJBUXdGQURCM01Rc3dDUVlEVlFRR0V3SlYKVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1TFVaeVlXNWphWE5qYnpFWgpNQmNHQTFVRUNoTVFUR2x1ZFhndFJtOTFibVJoZEdsdmJqRU5NQXNHQTFVRUN4TUVUMDVCVURFUk1BOEdBMVVFCkF4TUliMjVoY0M1dmNtY3dIaGNOTWpBeE1ERTJNRGt3TmpVeVdoY05NekF4TURFME1Ea3dOalV5V2pCM01Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1TFVaeQpZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFUR2x1ZFhndFJtOTFibVJoZEdsdmJqRU5NQXNHQTFVRUN4TUVUMDVCClVERVJNQThHQTFVRUF4TUliMjVoY0M1dmNtY3dnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUsKQW9JQ0FRQ0RCUjA2U1NQbXhVaXVnNTQvWGtaYlRTdmUxODNlRGIrck9iQU9XdTFjM3lRSEJqQkFBRUNhNEl1cQpUWkd3Tm9LL3ZZWHIyaXJ5UTAyTHBwNzd6QkN5cFZDRHJIR2wxNXdIcGtDWWpOTm91a29ZSGhhK3ZDRXN0bmxoClRMQlB5ZXJRcGRjZXJIc1VUYUhwaGpka3BmTGtsRnJmRno2U0NvMWt2SW5naEZBRVJsak9hTjMvaXEyNzFJQVQKZXB5QVZEZFR6USt4ek1CTlFGZ0YzUVVPUmgxNjVJSjRRZDlaVmNYY2pHd0lMR1Y5bHc0QWFJU2pWcUlwa2JMaApwd2puQTRQbUxkWnZIcjd5elQ1R014UFk3UVY5LzdOUWZrbk9UT1NacUZYMmRwc3FYZDdtTnYvRzA4MXpEYkpaCmJkeVVIeUFxUG00STdyWis2ZnJINzhQb0NId0FwMW1PUDVBelRLRVlVZW4xUEIrODhsVHFsbWp4bjhWWHo4dk4KNTVmSTRZQ1FINnRsUnV3UWpsMUhReUlQRFhqaDhPSklJbjRJZzlheTlGTTlDUy9KdzdIa09iakltaEFNMk1uUQpKbkNBc092WHluNGpkYnNHaWhab1hGMzg3T2dMdFdDanp3WlpNTUJPOEZuYm5jWWdXZWNibllwRXJyN05aeHIxCkUzcUIzSlRzWTVUQUltTjNOdnJGSXl1b3ZmNTRkeXJEV1FodGxlMGN1bmVCQlM1N0hTZ1hTZURqdlZKOFdyNTEKcGZSa2RNQm5BNFpZeEpkWmpraVcxb2NUSWV4d1drMXVQbTAvd0RVbFcrcHB5c0tIVDVwMjkwTmt0UlVjQjBieApQNGM5MzhJdW1DTmVOWU9XaVBDQXBlQ1JpZjg2MExuaDFkM1RxRy9XUDBiVGNYMkhBUUlEQVFBQm96SXdNREFkCkJnTlZIUTRFRmdRVVpBME4yK0tOQWVoTHFZNytDTWw2QmUzVDl5d3dEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU4KQmdrcWhraUc5dzBCQVF3RkFBT0NBZ0VBTTFRRGhDNmRKendFZTBzZjh4NmlwK2MvTEhBRWxPT1dYNytOL1FSdQppWmFjY2Znb3g2YWR1NEJFK2w5bVVycUt4RkJucG9tenZvTGZTcnNPa2poajFHNXVPakl1eEFSWm5LcmN3STRqCmMrV3VjU3FCQm5EcXl6TDcrN0cxVW5tNSt5aWZsNUFFczJ4KzdmdEZ6b2dVS1dhOTN4c1EyMmFOZ0RPejArQjYKRkwrVlBDMEpTTEgyUUdUdEhKVk1LaUxLQWoxTTFyQXNpdUlTSzVLS21rOUNHRkpsMkhBZ1dLNExUNGNiWlBUOAoyQk9ETEthVzJxRlZSSlJDUkpVQjlIWkdEeitGbjlNeE5YbVFmMG94KytIeWNKbGdRTXNJRE9VSmo2QjhieWdJCmVVMHBENTBSTUtOQyt0bmFlSExSS0xyR0ExS2FXOWtrdTBVTy9kSU5NZG1JZmk5RktHYlVBb0xoLzJjUjFiVUYKWFF0aUtlS3NILzBIV1EvTTJpVXBIYVNTZ3greHpOeC80d2FPTDhXZFphdGplcVZjYlJIbHkvbGs2bXErV0U4MQozOGk5ck1aTWlId1ZLaGJ6dXdZbVE0R0x1QWRRL1J0dHJVTE0xLzRGb2hNaFVwbHZ1Z3Z4K2ZhanFZZjg1a05SCm9rZHVGeE9YdDNNYzJyR2R0am8vY0N1cjFzeUtjalhuQitzWW16QWJQMlpTa0QwTG0rRjdkcFAyRzQ2ZkwvYUsKVFRMSFJLcVZTR3pDaXpDdWtQTU9kby9MakJ4ck9NVWR1QnVuZ1NFbnBxR09DR3c4bi83ZGpWSWxsN2VEbWk4ZApjNG9uZDhjemJxTGhjTWdhVWtqNmhVMklEeEdUTi9Ic3h4eDhxNU1TdzU3cElBdnVUNm9IeEVDdUdKSDR0aVU5ClFTND0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=")
+       CsrBytes, _            = base64.StdEncoding.DecodeString("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQy9EQ0NBZVFDQVFBd2JqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQWdUQ2tOaGJHbG1iM0p1YVdFeApDekFKQmdOVkJBY1RBbFZUTVEwd0N3WURWUVFLRXdSdmJtRndNUkF3RGdZRFZRUUxFd2R2Ym1Gd0xXOTFNUnd3CkdnWURWUVFERXhOalpYSjBhWE56ZFdWeUxtOXVZWEF1YjNKbk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0MKQVE4QU1JSUJDZ0tDQVFFQTJSZDFCV3JIRm5wUGdkdy9VaFBOYWZPWjB1S3lGZytuWHNNQUJEbFB6YzBxaWRWWQp4RTVmU0E0dUFXUmpvV1FLQ1dxQWxGZS9LYjBSL3ZlTTN6K0hUMUQrRXBsNjNZWUF5dVYyNFdaa2JEeDhGdGV6CjBqd2l1R21Zb0lld1JXMmZXY25RcTV6WDZ4LzEyalJ3eThtQVpJaFRtUXloTjRQWGpjd09ZcVJrVTBQeGsrQnAKMmt4RWpTQi9SSlJGKzE5YWh6K2IreStLQ0dJRVdiSStXb0RpMzFNYmh0aVVrMnd2MXdzUk8vbmRnM1RxT2Y0Twp5QjNtVGlYMkIyL0t1ZXpLbFlseENobUdjS1UxdTUwU0pYT3JZYU9KNTZJemdDTU1FVk1YaEpzYlgweFlnMkZMCjZsSkxXcjlma3pxeFRmenMxYUdVWXdDcG9rWHROa1UybXBPT1lRSURBUUFCb0Vrd1J3WUpLb1pJaHZjTkFRa08KTVRvd09EQXBCZ05WSFJFRUlqQWdnZ2xzYjJOaGJHaHZjM1NDRTJObGNuUnBjM04xWlhJdWIyNWhjQzV2Y21jdwpDd1lEVlIwUEJBUURBZ1dnTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDNThJWkt5dlFwdUFuVHR3NUd1eVh2ClQralNML240Kzg1b1dwamwxYVZnQTVRcWVHWU8wRzVQbzNMbGVLeTlCaEJIclBmY201eEFudFUvM0VKWmJBSFQKai9WWkRzdmVaR3JEc2hqRWI4dmNuSTRROXVpY0dNYnlUbktFcVpzSm5EMlpqN1RiWFBocXQrV1Z5S0RJc0ZLdAprNHVSWGpaRTI0VVh6ZFhiWnNUeWlscFl0RzJkR3RTTXVSdll6NlR1eUlWRVZMNVBXRWRGak5VUVJSK2czTVpDCmtrc2pKSzkvSC9OZVl1TS9QN1BUWjZkRWFUY3c5UmtSdEVGazBPQlRxWUlyaUhmczJUMUlIdzdMaVl6NFhyUEQKVFBncHppM1IyZVFINDhzSDVMS1UvblQxUWtNbHZiS3RpcWdoZkx6eGQ0MXEzRTZxNkRZdGJybHN6eTVpeTFqOAotLS0tLUVORCBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K")
+       PkBytes, _             = base64.StdEncoding.DecodeString("LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRRFpGM1VGYXNjV2VrK0IKM0Q5U0U4MXA4NW5TNHJJV0Q2ZGV3d0FFT1UvTnpTcUoxVmpFVGw5SURpNEJaR09oWkFvSmFvQ1VWNzhwdlJIKwo5NHpmUDRkUFVQNFNtWHJkaGdESzVYYmhabVJzUEh3VzE3UFNQQ0s0YVppZ2g3QkZiWjlaeWRDcm5OZnJIL1hhCk5IREx5WUJraUZPWkRLRTNnOWVOekE1aXBHUlRRL0dUNEduYVRFU05JSDlFbEVYN1gxcUhQNXY3TDRvSVlnUloKc2o1YWdPTGZVeHVHMkpTVGJDL1hDeEU3K2QyRGRPbzUvZzdJSGVaT0pmWUhiOHE1N01xVmlYRUtHWVp3cFRXNwpuUklsYzZ0aG80bm5vak9BSXd3UlV4ZUVteHRmVEZpRFlVdnFVa3RhdjErVE9yRk4vT3pWb1pSakFLbWlSZTAyClJUYWFrNDVoQWdNQkFBRUNnZ0VBWk04NHZ5QTdmUnVsQ2hlZHE5NllOOGd3T1RhZUhoSjgxVXRXR2FBSGgvanEKOVFDR2JQbzcwcmtLOGdpTkgyZldKVk00akNwSEVmbkRmcE96N2dPUk1PcmFZUWEyZ0dIMndrRldPQXNWUFJIRgpTZEkycGJ6WkhxdWlmWUVsQU1pTUErVHNxcFIxeTdDV3VSSTdBdGI2Y1RUQkpVUXhKUmRySkdTS2xaSGpLS3A4Clo4V0xkNllqTnVzSlY0c1ZNb0pTR3RPc0tkcUx0RytlYnhPK2RCdWNnbm95S01KMi9LREl2bmxHWE1CME5IOUYKaXVOSVFYNnlReHI0VlNGSVZndldwZ0dPYTI0QWtxSlFGRm80UVdlc1JVa01EaFU5aTJPYWc4ekNxL1U4VmFpMQo2eTBTa2FucTBxbVFoN1c5UFV5RXlKdGhhbk5RczYzNDZXWkZmNWY0bFFLQmdRRHc1MzRLZFNiYkJjQTBZVmpaCjI1NDRLbE1MMzBSc04vN2JIdEt0djVVKzIyYjBXekRycHVVbFpVUmVFZGducGlBQVg3RUpLdFRDUVVCektuTmQKU1g4WTE4WTB6cEkyZnhJb2pjN0YwVy9aMzB0bkM1NjNKLzV0WTk4eVJzQ3lISXpRTXQvOTMzUVRLU2pGaktBcwp1ZCtKbDR6Q25yWUpnVitrTGV6V1R4c1ptd0tCZ1FEbXNmaGZPOUF6V3diQS9IS09aL01ZaTRjcWE5disyN2xSCktmYStZQ3ZMRUE0UWhJRTZXVFA2ZGFtTnpwZytQMW9QcmdmSVBpNVJOelRmdlFoSzlMaW56dDdxVTJYbWJYVXUKNDJpR2p3UklwY1hLaGxYeFNCTTFQVGZ5dW5GaXlORW1qVlR2SWdudW9vNmdJNUp0RVNLQ2hGZy95YWIwRm9ONQpVRnV1MGp2bHN3S0JnRUpKWUZ3ZVNqZkFDRmdoWlNKbEZNOGRqa1paQStuSEtxQStoZmY3SEdUMFdBcnF3TFpHCjhReHVKZmJBY0RyUXNrT0lFUjJWcEg5akZ3blpaMjhHMXlzTnpHTWhhQWdJeFFWVnA4eTB5Vk1vNXdXT28vaC8KejdsbjNyVmwxSVh0NXkwdW9vV25vN2ZWL25zRks5bkN0MmlUdzg2VmZ6OTBVczNKT1Q3cSsyajdBb0dBUVFpTQp0dlFhcGsrVDROV0p5Y0ZlRTE1S0ZWaGdwVUQxeGY2cGMxT1RKT1I2d29kSUV0WFF4RnRsRi9mVWpUKzR1TkRiCm1zU0V0QnAzQ2xlMHZjU3RSWWtZNkQvb2F3UVNVOHlCeStVSFZSOStXYkJ6QzlqQXFYSi9raXFqQ2pFSVhQRGMKcjZrTjJicnpzQXMzSFE0R2gzcWRraVhicmRXbTdJME51NFBDcE9jQ2dZRUFzNUh1aFZ3Q0lrL0IrY0I0Um5RbwpXWTJOQjVPd1FpbmVmd3RVVlJJTWxGTkhWeWljTmhyME5wQkJ0TGF0RFRZOTlYRmx3eHh3LzMrM0hBbUdxTjVvCmNvVTVkQ3dNRWo0RmZ6V3ZIUTBWa0VsRVRkZ3ZnVFluejFYU015alJXZjZweTRaTXpBZ0xJL2pDQXlGMnQvMVMKZ1ZIR01LRFV0YjdNRHIzamg3ZmxoUG89Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K")
+)