From 8640bfd560698fc68e89d611ac4deb01a0bd1961 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Wed, 4 Sep 2024 11:45:40 +0200 Subject: [PATCH] Revert "Remove certificate logic (#32)" This reverts commit a39e59e8dde875ec0895c50d88aba9b5de094889. --- .github/workflows/renew-certificate.yml | 67 +++++++++++++ local-certs/.gitignore | 4 + local-certs/README.md | 20 ++++ local-certs/certificate-domains | 15 +++ local-certs/certificate-regions | 6 ++ local-certs/generate-certificate.sh | 26 ++++++ local-certs/generate-domains.py | 32 +++++++ local-certs/server.key | 119 ++++++++++++++++++++++++ 8 files changed, 289 insertions(+) create mode 100644 .github/workflows/renew-certificate.yml create mode 100644 local-certs/.gitignore create mode 100644 local-certs/README.md create mode 100644 local-certs/certificate-domains create mode 100644 local-certs/certificate-regions create mode 100755 local-certs/generate-certificate.sh create mode 100755 local-certs/generate-domains.py create mode 100644 local-certs/server.key diff --git a/.github/workflows/renew-certificate.yml b/.github/workflows/renew-certificate.yml new file mode 100644 index 0000000..ab572e7 --- /dev/null +++ b/.github/workflows/renew-certificate.yml @@ -0,0 +1,67 @@ +name: Renew Certificate +on: + workflow_dispatch: + pull_request: + paths: + - ".github/workflows/renew-certificate.yml" + - "local-certs/certificate-domains" + - "local-certs/certificate-regions" + - "local-certs/generate-domains.py" + - "local-certs/generate-certificate.sh" + branches: + - master + push: + paths: + - ".github/workflows/renew-certificate.yml" + - "local-certs/certificate-domains" + - "local-certs/certificate-regions" + - "local-certs/generate-domains.py" + - "local-certs/generate-certificate.sh" + branches: + - master + schedule: + # * is a special character in YAML so you have to quote this string + - cron: '0 6 1 * *' + +env: + git_user_name: localstack[bot] + git_user_email: localstack-bot@users.noreply.github.com + +permissions: + contents: write + +jobs: + renew-certificate: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python + id: setup-python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install Certbot + run: | + python -m pip install --upgrade pip wheel setuptools + pip install certbot certbot-plugin-gandi + - name: Generate certificate + working-directory: "local-certs" + env: + CERTBOT_ARGS: "${{ github.ref != 'refs/heads/master' && '--staging' || '' }}" + DNS_API_KEY: "${{ secrets.DNS_API_KEY }}" + CERTBOT_EMAIL: ${{ env.git_user_email }} + run: | + ./generate-certificate.sh + - name: Commit certificate + working-directory: "local-certs" + if: github.ref == 'refs/heads/master' + run: | + git config user.name ${{ env.git_user_name }} + git config user.email ${{ env.git_user_email }} + git add server.key + expiry_date=$(date --date="$(openssl x509 -enddate -noout -in server.key | cut -d= -f 2)" --utc --iso-8601) + git commit -m "update local certificate keys (new expiry date: $expiry_date)" + git push + \ No newline at end of file diff --git a/local-certs/.gitignore b/local-certs/.gitignore new file mode 100644 index 0000000..6ad9285 --- /dev/null +++ b/local-certs/.gitignore @@ -0,0 +1,4 @@ +gandi.ini +config/ +logs/ +work/ diff --git a/local-certs/README.md b/local-certs/README.md new file mode 100644 index 0000000..6c56a6f --- /dev/null +++ b/local-certs/README.md @@ -0,0 +1,20 @@ +## Certificate renewal +The current certificate for `localhost.localstack.cloud` and several of its subdomains are stored in `server.key`. + +The file contains both certificate and private key. + +### Limitations +Please make sure to conform to the [LetsEncrypt Rate Limits](https://letsencrypt.org/docs/rate-limits/). + +Most notably, do not rerequest the certficate for the same set of domain names more than 5 times a week, for new domain names more than 50 times a week and no more than 100 names in total per certificate. + + +### Domain lists + +* `certificate-domains` contains a list (newline separated) with all domains the certificate should be valid for. It allows `{region}` as placeholder, to generate domains for multiple regions. +* `certificate-regions` contains a list (newline separated) of all regions which will be substituted for the `{region}` placeholder. + + +### Timeline + +The certificate renewal will happen every time the domain/region list are updated, or every month. \ No newline at end of file diff --git a/local-certs/certificate-domains b/local-certs/certificate-domains new file mode 100644 index 0000000..41dc95f --- /dev/null +++ b/local-certs/certificate-domains @@ -0,0 +1,15 @@ +localhost.localstack.cloud +*.localhost.localstack.cloud +*.amplifyapp.localhost.localstack.cloud +*.cloudfront.localhost.localstack.cloud +*.elb.localhost.localstack.cloud +*.execute-api.localhost.localstack.cloud +*.opensearch.localhost.localstack.cloud +*.{region}.opensearch.localhost.localstack.cloud +*.s3-website.localhost.localstack.cloud +*.s3.localhost.localstack.cloud +*.scm.localhost.localstack.cloud +*.dkr.ecr.{region}.localhost.localstack.cloud +*.lambda-url.{region}.localhost.localstack.cloud +sqs.{region}.localhost.localstack.cloud +*.snowflake.localhost.localstack.cloud diff --git a/local-certs/certificate-regions b/local-certs/certificate-regions new file mode 100644 index 0000000..e3dcbc4 --- /dev/null +++ b/local-certs/certificate-regions @@ -0,0 +1,6 @@ +eu-central-1 +eu-west-1 +us-east-1 +us-east-2 +us-west-1 +us-west-2 \ No newline at end of file diff --git a/local-certs/generate-certificate.sh b/local-certs/generate-certificate.sh new file mode 100755 index 0000000..4d279d9 --- /dev/null +++ b/local-certs/generate-certificate.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +# generate comma separated list of all domains to request a cert for +certificate_domains=$(python generate-domains.py) +echo "Generating certificate for domains ${certificate_domains}" + +# create credentials file +echo "dns_gandi_api_key=${DNS_API_KEY}" > gandi.ini +chmod 600 gandi.ini + +# request certificate +set -x +certbot -n --agree-tos --email ${CERTBOT_EMAIL} ${CERTBOT_ARGS} --authenticator dns-gandi --dns-gandi-credentials gandi.ini --key-type rsa --work-dir=$PWD/work --config-dir=$PWD/config --logs-dir=$PWD/logs -d $certificate_domains certonly +set +x + +# remove credentials to avoid accidental leakage +rm gandi.ini + +# concatinate private key + cert into single file to match current structure +echo "Concatinating certificate and key into single file" +cat config/live/localhost.localstack.cloud/privkey.pem config/live/localhost.localstack.cloud/fullchain.pem > server.key + +# display certificate information +echo "Certificate information" +openssl x509 -in server.key -text -noout diff --git a/local-certs/generate-domains.py b/local-certs/generate-domains.py new file mode 100755 index 0000000..0977380 --- /dev/null +++ b/local-certs/generate-domains.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +""" +This file generates a comma-separated list of domains, +by replacing the occurences of "{region}" in the domains +in "certificate-domains" by the regions in "certificate-regions" +""" + + +REGION_PLACEHOLDER = "{region}" + + +def generate_domains(domain_list: list[str], region_list: list[str]) -> list[str]: + result = [] + for domain in domain_list: + if REGION_PLACEHOLDER in domain: + result += [domain.replace(REGION_PLACEHOLDER, region) for region in region_list] + else: + result.append(domain) + return result + +def main(): + with open("certificate-domains", mode="rt") as f: + domain_list = f.read().splitlines() + with open("certificate-regions", mode="rt") as f: + region_list = f.read().splitlines() + expanded_list = generate_domains(domain_list, region_list) + print(",".join(expanded_list), end="") + + +if __name__ == "__main__": + main() diff --git a/local-certs/server.key b/local-certs/server.key new file mode 100644 index 0000000..34e7393 --- /dev/null +++ b/local-certs/server.key @@ -0,0 +1,119 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkPpgky1WMi9DO +ujPv3icBhHTV/tKeAWl+7ZuM0BkQMhWHCz2MYtOCIbL1jbH41UOmKmNjHglbNO06 +sxdWbMuwgpY/nE9fUEW3AYxsoK/PKB/IpJ4LmuCLeHt4RlqXG8BPeSX6UeTP08/8 +yRToaZTgCNjrP30uk3FSQDIf6wsxKhQMM88MEQOQXkRGS0B7Cfaz2fFmXiKeFYAo +JUYwsS0zVpPwqFVJtH8nEHG17Szezqj1KWRYbIxHSoMOpU06ogFDT6EF/gHtR3N6 +ggafd6De3KlKkHthbBARskdTuAK0TUV4MfaA+IRElDMxdCl2x9rHRSxKBVySYMwI +Hd0FYHX/AgMBAAECggEADhK8954ioO3UAPqse5el8PfYCQQSKO7HCtSyOOEV3LDl +7lHUvsQzCln32xl+j+s6JjFIndQTiiihUR/KVqHw3Bl8ZUvv8yNOLe2oiiBEoDP4 +cTlFv4nQRrMWpUol9f+vPUAMtIOy3unvzuGt9HIjiwTHDXU9tTF2Cs2sXdbGeTpW +ELL2SG1FXMLH08a66eIundAXMbtu06N8Mb36oDfx2rE/F77lozUmvoNWZ9W9BUW3 +6zK+VRG6MO5TqaBN4BhKQ8gxr9w/ovh6z9aKEgHtvhE8c1Jjnt19vG6yy1A/VI8j +hSjIFZUIi/PE6VZCqs05uE7bb0ut9+5+yWBYbCaj3QKBgQDbITMAwAjTProm7eYk +GzIKegckRddv5Q06FfujOIoEpjnauXNCS3UHbkusDxr1WWnYOIabyQ/aUMaVffY6 +VB+vZat8bV8T8sN3c4Zqkyoq5M7V8Z64A3vxrUbIwArAqh48/lS78D0Y60ItXkVD +VIK/LQ/zQ3x6UklD4Ev9WdRXxQKBgQC/4UTS+Y+G0iW9ig+CsRO7/m98vZJGW0n5 +Skvm7qUGGmhurfEnOsDB+i8Qf9wMNj3xW+P5GNpkeo969XU48sPElcAog6c6Dhz3 +HLCHtrArpULX/qoZ7VphRgP/xePjhYvqTlBT33rt2HODcChys7NuF6+GSld/9RQO +pXRhtdju8wKBgQDVvWNXZvj0vGmtET37l/9Oksqmie1jOoOVVd32zm5prI4gF+Yi +EyIa1m2/bZh5GvcQLcq25/6rj0C0joH/URD67+u+WZx1A8W/nRLOn69w2XAa5SxR +Byz9hmvV4uRaG1WVXurdyq59wPPy9tIOo79IpLa7LOedFOhb6cuVWuqxlQKBgF/F +iXugUpIhe/Lh4SIDTm1L7sudN1BkqkSCX+YxAS9NqQhtS2ugOKvZOvqKRwPVYw+A +JQak8ASs48akk1DMYwhREmtmYuZoOu7gZAApID94qidzFeYBAVaCAub8F+XtN0vI +sPFcH1ht7CITriyqIwn1SofFvWzBn7Q7wx4uDMwfAoGAe4IanzZZgk2lIopbtUb5 +LuhH+22O+LUU3iZmftgQpAmVcwYy4DP8Sa8IgsH3Xn6QGlKKVVl17FEcuGev50/c +SqZeazLX6Zf/JoDEGPX25SBgeTGj+vV7NvMi/qSFmW0CuW2uFb64F2vAyu4wsix5 +KvSSF8+a+Xqp5EXGFQNsj+4= +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIILDzCCCfegAwIBAgISA8X5/0JomSBgXTptip67SBc9MA0GCSqGSIb3DQEBCwUA +MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD +EwNSMTAwHhcNMjQwODI3MDgxMTQzWhcNMjQxMTI1MDgxMTQyWjAlMSMwIQYDVQQD +Expsb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZDCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAKQ+mCTLVYyL0M66M+/eJwGEdNX+0p4BaX7tm4zQGRAyFYcL +PYxi04IhsvWNsfjVQ6YqY2MeCVs07TqzF1Zsy7CClj+cT19QRbcBjGygr88oH8ik +ngua4It4e3hGWpcbwE95JfpR5M/Tz/zJFOhplOAI2Os/fS6TcVJAMh/rCzEqFAwz +zwwRA5BeREZLQHsJ9rPZ8WZeIp4VgCglRjCxLTNWk/CoVUm0fycQcbXtLN7OqPUp +ZFhsjEdKgw6lTTqiAUNPoQX+Ae1Hc3qCBp93oN7cqUqQe2FsEBGyR1O4ArRNRXgx +9oD4hESUMzF0KXbH2sdFLEoFXJJgzAgd3QVgdf8CAwEAAaOCCCkwggglMA4GA1Ud +DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T +AQH/BAIwADAdBgNVHQ4EFgQUjL+KDrAll1/D9srF4e6/GwekaA0wHwYDVR0jBBgw +FoAUu7zDR6XkvKnGw6RyDBCNojXhyOgwVwYIKwYBBQUHAQEESzBJMCIGCCsGAQUF +BzABhhZodHRwOi8vcjEwLm8ubGVuY3Iub3JnMCMGCCsGAQUFBzAChhdodHRwOi8v +cjEwLmkubGVuY3Iub3JnLzCCBi4GA1UdEQSCBiUwggYhgicqLmFtcGxpZnlhcHAu +bG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCJyouY2xvdWRmcm9udC5sb2NhbGhv +c3QubG9jYWxzdGFjay5jbG91ZIIxKi5ka3IuZWNyLmV1LWNlbnRyYWwtMS5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIuKi5ka3IuZWNyLmV1LXdlc3QtMS5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIuKi5ka3IuZWNyLnVzLWVhc3QtMS5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIuKi5ka3IuZWNyLnVzLWVhc3QtMi5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIuKi5ka3IuZWNyLnVzLXdlc3QtMS5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIuKi5ka3IuZWNyLnVzLXdlc3QtMi5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIgKi5lbGIubG9jYWxob3N0LmxvY2Fsc3Rh +Y2suY2xvdWSCNCouZXUtY2VudHJhbC0xLm9wZW5zZWFyY2gubG9jYWxob3N0Lmxv +Y2Fsc3RhY2suY2xvdWSCMSouZXUtd2VzdC0xLm9wZW5zZWFyY2gubG9jYWxob3N0 +LmxvY2Fsc3RhY2suY2xvdWSCKCouZXhlY3V0ZS1hcGkubG9jYWxob3N0LmxvY2Fs +c3RhY2suY2xvdWSCNCoubGFtYmRhLXVybC5ldS1jZW50cmFsLTEubG9jYWxob3N0 +LmxvY2Fsc3RhY2suY2xvdWSCMSoubGFtYmRhLXVybC5ldS13ZXN0LTEubG9jYWxo +b3N0LmxvY2Fsc3RhY2suY2xvdWSCMSoubGFtYmRhLXVybC51cy1lYXN0LTEubG9j +YWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCMSoubGFtYmRhLXVybC51cy1lYXN0LTIu +bG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCMSoubGFtYmRhLXVybC51cy13ZXN0 +LTEubG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCMSoubGFtYmRhLXVybC51cy13 +ZXN0LTIubG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCHCoubG9jYWxob3N0Lmxv +Y2Fsc3RhY2suY2xvdWSCJyoub3BlbnNlYXJjaC5sb2NhbGhvc3QubG9jYWxzdGFj +ay5jbG91ZIInKi5zMy13ZWJzaXRlLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vk +gh8qLnMzLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgiAqLnNjbS5sb2NhbGhv +c3QubG9jYWxzdGFjay5jbG91ZIImKi5zbm93Zmxha2UubG9jYWxob3N0LmxvY2Fs +c3RhY2suY2xvdWSCMSoudXMtZWFzdC0xLm9wZW5zZWFyY2gubG9jYWxob3N0Lmxv +Y2Fsc3RhY2suY2xvdWSCMSoudXMtZWFzdC0yLm9wZW5zZWFyY2gubG9jYWxob3N0 +LmxvY2Fsc3RhY2suY2xvdWSCMSoudXMtd2VzdC0xLm9wZW5zZWFyY2gubG9jYWxo +b3N0LmxvY2Fsc3RhY2suY2xvdWSCMSoudXMtd2VzdC0yLm9wZW5zZWFyY2gubG9j +YWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCGmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNs +b3VkgitzcXMuZXUtY2VudHJhbC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vk +gihzcXMuZXUtd2VzdC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgihzcXMu +dXMtZWFzdC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgihzcXMudXMtZWFz +dC0yLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgihzcXMudXMtd2VzdC0xLmxv +Y2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgihzcXMudXMtd2VzdC0yLmxvY2FsaG9z +dC5sb2NhbHN0YWNrLmNsb3VkMBMGA1UdIAQMMAowCAYGZ4EMAQIBMIIBBAYKKwYB +BAHWeQIEAgSB9QSB8gDwAHYASLDja9qmRzQP5WoC+p0w6xxSActW3SyB2bu/qznY +hHMAAAGRkxmXAAAABAMARzBFAiAekzpZ4BDa1pqeUT7RmPK/m+MY8KMNabKFg/AW +lAdDBQIhAOXsXbLaXnUbLXDr4Sx5p6VXFMHJ8d4Iyleny0QzvZMWAHYAdv+IPwq2 ++5VRwmHM9Ye6NLSkzbsp3GhCCp/mZ0xaOnQAAAGRkxmXUwAABAMARzBFAiAb8ttM +ztUxiVtgiF0R9WZ8Bmo7q75rLd+q+IOuVcSySwIhAJjmKMGXopkCr5HT3p1WT3MP +1thDnNBYWL+0G0wKK1/OMA0GCSqGSIb3DQEBCwUAA4IBAQCmo2uic9k6jUNJluPS +07aZVZZtI8a3yV1/swTNucYWg0YcF5Wbv5d2LH1QXeLThVrMRow4uI/GMkOmiQYb +WJC9i3YdUf1rlBsi+/oQuXxGOh4l23ZhCjB+Ntp7XP69Sdg0qmd0v6qP6vrdsCRJ +q5HD26LR5Gw55DgApomVpdQa9AYgoF2aeKZbJ9RG1M6Fu4LoC5d4hw3QfYGSJd85 +qttWgU+2dAGb70FhQO1dGQZwlFCSWEn0RtOVXhstw6TspP3i9lqsLyhPvWxs0TDX +H1mKmInXK+uo7B0YT/n2oZ9TVjqyuHQ9t30VFS4Ixslr/n7UlLhUV2msflQgUAa3 +uaDS +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFBTCCAu2gAwIBAgIQS6hSk/eaL6JzBkuoBI110DANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy +Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa +Fw0yNzAzMTIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF +bmNyeXB0MQwwCgYDVQQDEwNSMTAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDPV+XmxFQS7bRH/sknWHZGUCiMHT6I3wWd1bUYKb3dtVq/+vbOo76vACFL +YlpaPAEvxVgD9on/jhFD68G14BQHlo9vH9fnuoE5CXVlt8KvGFs3Jijno/QHK20a +/6tYvJWuQP/py1fEtVt/eA0YYbwX51TGu0mRzW4Y0YCF7qZlNrx06rxQTOr8IfM4 +FpOUurDTazgGzRYSespSdcitdrLCnF2YRVxvYXvGLe48E1KGAdlX5jgc3421H5KR +mudKHMxFqHJV8LDmowfs/acbZp4/SItxhHFYyTr6717yW0QrPHTnj7JHwQdqzZq3 +DZb3EoEmUVQK7GH29/Xi8orIlQ2NAgMBAAGjgfgwgfUwDgYDVR0PAQH/BAQDAgGG +MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATASBgNVHRMBAf8ECDAGAQH/ +AgEAMB0GA1UdDgQWBBS7vMNHpeS8qcbDpHIMEI2iNeHI6DAfBgNVHSMEGDAWgBR5 +tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKG +Fmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0gBAwwCjAIBgZngQwBAgEwJwYD +VR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVuY3Iub3JnLzANBgkqhkiG9w0B +AQsFAAOCAgEAkrHnQTfreZ2B5s3iJeE6IOmQRJWjgVzPw139vaBw1bGWKCIL0vIo +zwzn1OZDjCQiHcFCktEJr59L9MhwTyAWsVrdAfYf+B9haxQnsHKNY67u4s5Lzzfd +u6PUzeetUK29v+PsPmI2cJkxp+iN3epi4hKu9ZzUPSwMqtCceb7qPVxEbpYxY1p9 +1n5PJKBLBX9eb9LU6l8zSxPWV7bK3lG4XaMJgnT9x3ies7msFtpKK5bDtotij/l0 +GaKeA97pb5uwD9KgWvaFXMIEt8jVTjLEvwRdvCn294GPDF08U8lAkIv7tghluaQh +1QnlE4SEN4LOECj8dsIGJXpGUk3aU3KkJz9icKy+aUgA+2cP21uh6NcDIS3XyfaZ +QjmDQ993ChII8SXWupQZVBiIpcWO4RqZk3lr7Bz5MUCwzDIA359e57SSq5CCkY0N +4B6Vulk7LktfwrdGNVI5BsC9qqxSwSKgRJeZ9wygIaehbHFHFhcBaMDKpiZlBHyz +rsnnlFXCb5s8HKn5LsUgGvB24L7sGNZP2CX7dhHov+YhD+jozLW2p9W4959Bz2Ei +RmqDtmiXLnzqTpXbI+suyCsohKRg6Un0RC47+cpiVwHiXZAW+cn8eiNIjqbVgXLx +KPpdzvvtTnOPlC7SQZSYmdunr3Bf9b77AiC/ZidstK36dRILKz7OA54= +-----END CERTIFICATE-----