From ee23e5f54f96807b1f1fff0b45238a247d3dd8e0 Mon Sep 17 00:00:00 2001 From: Remigiusz Janeczek Date: Thu, 22 Oct 2020 09:18:12 +0200 Subject: [PATCH] [OOM-K8S-CERT-EXTERNAL-PROVIDER] Add client for CertService API Issue-ID: OOM-2559 Signed-off-by: Remigiusz Janeczek Change-Id: I3bf6c36b9eec7a661202b18eb7765e332ccfbc07 --- .../deploy/_certificate_example_.yaml | 18 ++-- .../src/certserviceclient/cert_service_client.go | 73 +++++++++++++++ .../cert_service_client_factory.go | 74 +++++++++++++++ .../cert_service_client_factory_test.go | 95 +++++++++++++++++++ .../certserviceclient/cert_service_client_test.go | 101 +++++++++++++++++++++ .../certificate_request_controller.go | 29 +++++- .../src/cmpv2provisioner/cmpv2_provisioner.go | 39 ++++---- .../cmpv2provisioner/cmpv2_provisioner_factory.go | 15 ++- .../cmpv2_provisioner_factory_test.go | 29 +++--- .../src/cmpv2provisioner/cmpv2_provisioner_test.go | 54 ++++++----- .../expected_signed.pem | 0 .../expected_trusted.pem | 0 .../test_certificate.pem | 0 .../test_certificate_request.pem | 0 .../src/testdata/constants.go | 12 +++ 15 files changed, 471 insertions(+), 68 deletions(-) create mode 100644 certServiceK8sExternalProvider/src/certserviceclient/cert_service_client.go create mode 100644 certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_factory.go create mode 100644 certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_factory_test.go create mode 100644 certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_test.go rename certServiceK8sExternalProvider/src/cmpv2provisioner/{test_resources => testdata}/expected_signed.pem (100%) rename certServiceK8sExternalProvider/src/cmpv2provisioner/{test_resources => testdata}/expected_trusted.pem (100%) rename certServiceK8sExternalProvider/src/cmpv2provisioner/{test_resources => testdata}/test_certificate.pem (100%) rename certServiceK8sExternalProvider/src/cmpv2provisioner/{test_resources => testdata}/test_certificate_request.pem (100%) create mode 100644 certServiceK8sExternalProvider/src/testdata/constants.go diff --git a/certServiceK8sExternalProvider/deploy/_certificate_example_.yaml b/certServiceK8sExternalProvider/deploy/_certificate_example_.yaml index 60665d7f..e19e6351 100644 --- a/certServiceK8sExternalProvider/deploy/_certificate_example_.yaml +++ b/certServiceK8sExternalProvider/deploy/_certificate_example_.yaml @@ -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 index 00000000..870a3eda --- /dev/null +++ b/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client.go @@ -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 index 00000000..198f2294 --- /dev/null +++ b/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_factory.go @@ -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 index 00000000..50a6d796 --- /dev/null +++ b/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_factory_test.go @@ -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 index 00000000..1e15d43e --- /dev/null +++ b/certServiceK8sExternalProvider/src/certserviceclient/cert_service_client_test.go @@ -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) +} diff --git a/certServiceK8sExternalProvider/src/cmpv2controller/certificate_request_controller.go b/certServiceK8sExternalProvider/src/cmpv2controller/certificate_request_controller.go index 54b4b103..d526bbc8 100644 --- a/certServiceK8sExternalProvider/src/cmpv2controller/certificate_request_controller.go +++ b/certServiceK8sExternalProvider/src/cmpv2controller/certificate_request_controller.go @@ -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) diff --git a/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner.go b/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner.go index e48b527d..67d719cc 100644 --- a/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner.go +++ b/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner.go @@ -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 diff --git a/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_factory.go b/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_factory.go index 4a3898e7..125c1bc6 100644 --- a/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_factory.go +++ b/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_factory.go @@ -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) { diff --git a/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_factory_test.go b/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_factory_test.go index 6ef33098..1e215d3f 100644 --- a/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_factory_test.go +++ b/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_factory_test.go @@ -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 diff --git a/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_test.go b/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_test.go index f3ab5cb0..39e399b8 100644 --- a/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_test.go +++ b/certServiceK8sExternalProvider/src/cmpv2provisioner/cmpv2_provisioner_test.go @@ -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/cmpv2provisioner/test_resources/expected_signed.pem b/certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/expected_signed.pem similarity index 100% rename from certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/expected_signed.pem rename to certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/expected_signed.pem diff --git a/certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/expected_trusted.pem b/certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/expected_trusted.pem similarity index 100% rename from certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/expected_trusted.pem rename to certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/expected_trusted.pem diff --git a/certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/test_certificate.pem b/certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/test_certificate.pem similarity index 100% rename from certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/test_certificate.pem rename to certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/test_certificate.pem diff --git a/certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/test_certificate_request.pem b/certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/test_certificate_request.pem similarity index 100% rename from certServiceK8sExternalProvider/src/cmpv2provisioner/test_resources/test_certificate_request.pem rename to certServiceK8sExternalProvider/src/cmpv2provisioner/testdata/test_certificate_request.pem diff --git a/certServiceK8sExternalProvider/src/testdata/constants.go b/certServiceK8sExternalProvider/src/testdata/constants.go new file mode 100644 index 00000000..d2097bae --- /dev/null +++ b/certServiceK8sExternalProvider/src/testdata/constants.go @@ -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") +) -- 2.16.6