Commit 779568d5 authored by Maarten de Waard's avatar Maarten de Waard 🤘🏻
Browse files

Merge branch '4-remove-installer-code-and-make-python3-and-new-certbot-compatible' into 'develop'

Resolve "remove installer code and make python3 and new certbot compatible"

Closes #4

See merge request open/certbot-haproxy!1
parents 8a660350 b2a251ea
......@@ -32,6 +32,21 @@ configure HAProxy for use with the plugin. If you have a good idea on how we can
implement automatic HAProxy configuration, you are welcome to create a merge
request or an issue.
Dropped installer support in version 0.2.0+
------------------------------------------
In version 0.2.0 the installer component is dropped. Originally the installer
component made sure to place the certificates in the right directory for haproxy
by combining the key and the crt. This was done because original versions of
certbot executed the hooks after every domain renewal.
New versions of certbot have move fine grained post install hooks. With those
hooks more flexibility is added for installation. An example script and command
is added in version 0.2.0+
The example script for deploy is `certbot-deploy-hook-example`
Installing: Requirements
------------------------
......@@ -40,24 +55,25 @@ work on Ubuntu 14.04+ too. If you are running Debian Wheezy, you may need to
take additional steps during the installation. Thus, the requirements are:
- Debian Jessie (or higher) or Ubuntu Trusty (or higher).
- Python 2.7 (2.6 is supported by certbot and our goal is to be compatible but
- Python 3.0+ (Python 2.7 is still supported to be compatible with older
operating systems)
it has not been tested yet).
- HAProxy 1.6+ (we will configure SNI, which is not strictly required)
- Certbot 0.8+
- HAProxy 1.6+
- Certbot 0.19+
Installing: Getting started
---------------------------
The installation below assumes you are running Debian Jessie but it should be
The installation below assumes you are running Debian Stretch but it should be
almost entirely the same process on Ubuntu.
First add the backports repo for Jessie to your apt sources.
If you are still using Jessie, you have to add the backports repo for Jessie.
.. note::
This will not work for Ubuntu, you will need to use another source,
check which version comes with your version of Ubuntu, if it is a version
below 0.8, you need to find a back port PPA or download certbot from source.
below 0.19, you need to find a back port PPA or download certbot from source.
.. code:: bash
......@@ -82,7 +98,7 @@ Now update, upgrade and install some requirements:
openssl ca-certificates \
build-essential libffi-dev libssl-dev python-dev \
python python-setuptools \
haproxy
haproxy python3-pip python3-setuptools
easy_install pip
pip install --upgrade setuptools
......@@ -302,23 +318,22 @@ together a configuration that works for you.
systemctl restart haproxy
Now you can try to run Certbot with the plugin as the Authenticator and
Installer, if you already have websites configured in your HAProxy setup, you
Now you can try to run Certbot with the plugin as the Authenticator.
If you already have websites configured in your HAProxy setup, you
may try to install a certificate now.
.. code:: bash
certbot run --authenticator certbot-haproxy:haproxy-authenticator \
--installer certbot-haproxy:haproxy-installer
certbot certonly --authenticator certbot-haproxy:haproxy-authenticator \
--deploy-hook /path/to/your/install/script
If you want your ``certbot`` to always use our Installer and Authenticator, you
If you want your ``certbot`` to always use our Authenticator, you
can add this to your configuration file:
.. code:: bash
cat <<EOF >> $HOME/.config/letsencrypt/cli.ini
authenticator=certbot-haproxy:haproxy-authenticator
installer=certbot-haproxy:haproxy-installer
EOF
If you need to run in unattended mode, there are a bunch of arguments you need
......@@ -367,7 +382,7 @@ after the server has been offline for a long time.
[Service]
Type=simple
User=certbot
ExecStart=/usr/bin/certbot renew -q
ExecStart=/usr/bin/certbot renew -q --deploy-hook /path/to/deploy/script
EOF
# Enable the timer and start it, this is not necessary for the service,
......@@ -446,7 +461,6 @@ reasons.
text=True
domain=example.org
authenticator=certbot-haproxy:haproxy-authenticator
installer=certbot-haproxy:haproxy-installer
EOF
Setuptools version conflict
......@@ -476,11 +490,11 @@ Run the following commands in your vagrant machine:
.. code:: bash
apt-file update
python setup.py sdist
python3 setup.py sdist
# py2dsc has a problem with vbox mounted folders
mv dist/certbot-haproxy-<version>.tar.gz ~
cd ~
py2dsc certbot-haproxy-<version>.tar.gz
py2dsc --with-python3=True certbot-haproxy-<version>.tar.gz
cd deb_dist/certbot-haproxy-<version>
# NOTE: Not signed, no signed changes (with -uc and -us)
# NOTE: Add the package to the ghtools repo
......
#!/usr/bin/env python3
import os
import re
import sys
# Certbot sets an environment variable RENEWED_LINEAGE, which points to the
# path of the renewed certificate. We use that path to determine and find
# the files for the currently renewed certificated
lineage=os.environ.get('RENEWED_LINEAGE')
# If nothing renewed, exit
if not lineage:
sys.exit()
# From the linage, we strip the 'domain name', which is the last part
# of the path.
result = re.match(r'.*/live/(.+)$', lineage)
# If we can not recognize the path, we exit with 1
if not result:
sys.exit(1)
# Extract the domain name
domain = result.group(1)
# Define a path for HAproxy where you want to write the .pem file.
deploy_path="/etc/haproxy/ssl/" + domain + ".pem"
# The source files can be found in below paths, constructed with the lineage
# path
source_key = lineage + "/privkey.pem"
source_chain = lineage + "/fullchain.pem"
# HAproxy requires to combine the key and chain in one .pem file
with open(deploy_path, "w") as deploy, \
open(source_key, "r") as key, \
open(source_chain, "r") as chain:
deploy.write(key.read())
deploy.write(chain.read())
# Here you can add your service reload command. Which will be executed after
# every renewal, which is fine if you only have a few domains.
# Alternative is to add the reload to the --post-hook. In that case it is only
# run once after all renewals. That would be the use-case if you have a large
# number of different certificates served by HAproxy.
......@@ -90,18 +90,25 @@ CLI_DEFAULTS_DEBIAN_BASED_PRE_SYSTEMD_OS = dict(
CLI_DEFAULTS = {
"debian": {
'_min_version': '7',
'_max_version': '8',
'_max_version': '9',
'7': CLI_DEFAULTS_DEBIAN_BASED_PRE_SYSTEMD_OS,
'8': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS
'8': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS,
'9': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS
},
"ubuntu": {
'_min_version': '14.04',
'_max_version': '16.04',
'_max_version': '18.04',
'14.04': CLI_DEFAULTS_DEBIAN_BASED_PRE_SYSTEMD_OS,
'14.10': CLI_DEFAULTS_DEBIAN_BASED_PRE_SYSTEMD_OS,
'15.04': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS,
'15.10': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS,
'16.04': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS
'16.04': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS,
'16.10': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS,
'17.04': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS,
'17.10': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS,
'18.04': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS,
'18.10': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS,
'19.04': CLI_DEFAULTS_DEBIAN_BASED_SYSTEMD_OS
}
}
......
This diff is collapsed.
......@@ -12,9 +12,10 @@ class ConstantsTest(unittest.TestCase):
CLI_DEFAULTS = {
"debian": {
'_min_version': '7',
'_max_version': '8',
'_max_version': '9',
'7': 7,
'8': 8
'8': 8,
'9': 9
},
"ubuntu": {
'_min_version': '14.04',
......@@ -37,13 +38,13 @@ class ConstantsTest(unittest.TestCase):
)
@patch('certbot_haproxy.constants.CLI_DEFAULTS', new=CLI_DEFAULTS)
@patch('certbot.util.get_os_info', return_value=['debian', '9'])
@patch('certbot.util.get_os_info', return_value=['debian', '10'])
@patch('certbot_haproxy.constants.logger')
def test_os_analyse_unsupported_new(self, m_logger, *mocks):
""" Test an unsupported, too new version.. """
self.assertEqual(
constants.os_analyse(caching_disabled=True),
('debian', '8')
('debian', '9')
)
m_logger.warn.assert_called_once()
......@@ -61,17 +62,6 @@ class ConstantsTest(unittest.TestCase):
with self.assertRaises(NotSupportedError):
constants.os_analyse(caching_disabled=True)
@patch('certbot_haproxy.constants.CLI_DEFAULTS', new=CLI_DEFAULTS)
@patch('certbot.util.get_os_info', return_value=['ubuntu', '15.06'])
@patch('certbot_haproxy.constants.logger')
def test_os_analyse_between_versions(self, m_logger, *mocks):
""" Test a version in between our supported version numbers.. """
self.assertEqual(
constants.os_analyse(caching_disabled=True),
('ubuntu', '15.04')
)
m_logger.warn.assert_called_once()
if __name__ == '__main__':
unittest.main()
"""Test installer functions"""
from past.builtins import basestring
import unittest
import mock
import os
from certbot import errors
from certbot.plugins import common
from certbot_haproxy.installer import HAProxyInstaller
def _conf(self, var):
"""Don't append names to attributes in the config."""
return getattr(self.config, var.replace("-", "_"))
@mock.patch("certbot_haproxy.installer.HAProxyInstaller.conf", new=_conf)
class TestInstaller(unittest.TestCase):
test_domain = 'le.wtf'
"""Test the relevant functions of the certbot_haproxy installer"""
def setUp(self):
self.test_dir = "installer"
self.temp_dir, config_dir, work_dir = common.dir_setup(
test_dir=self.test_dir,
pkg="certbot_haproxy.tests")
backups = os.path.join(work_dir, "backups")
mock_le_config = mock.MagicMock(
temp_checkpoint_dir=os.path.join(
work_dir, "temp_checkpoints"),
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
work_dir=work_dir,
config_dir=config_dir,
temp_dir=self.temp_dir,
backup_dir=backups,
haproxy_config=os.path.join(
self.temp_dir, self.test_dir, "haproxy.cfg"),
haproxy_crt_dir=os.path.join(
self.temp_dir, self.test_dir, "certs"),
haproxy_ca_common_name=u'h2ppy h2cker fake CA',
no_fall_back_cert=False,
)
self.installer = HAProxyInstaller(
config=mock_le_config, name="installer")
self.installer.prepare()
def test_get_all_certs_keys(self):
"""Test if get_all_certs_keys returns all the LE certificates"""
all_certs_keys = self.installer.get_all_certs_keys()
self.assertEqual(len(all_certs_keys), 3)
self.assertIsInstance(all_certs_keys, list)
for item in all_certs_keys:
self.assertIsInstance(item, tuple)
@mock.patch("certbot_haproxy.installer.logger")
@mock.patch("certbot.util.logger")
def test_add_parser_arguments(self, util_logger, certbot_logger):
"""Weak test taken from apache plugin tests"""
self.installer.add_parser_arguments(mock.MagicMock())
self.assertEqual(certbot_logger.error.call_count, 0)
self.assertEqual(util_logger.error.call_count, 0)
def test_get_all_names(self):
"""Tests if get_all_Names reads le1.wtf and le2.wtf from the test
haproxy config file
"""
names = self.installer.get_all_names()
self.assertEqual(names, set(['le1.wtf', 'le2.wtf']))
def test_fall_back_cert(self, *mocks):
"""Test if a certificate is generated and added to new_crt_files"""
# Should maybe use another library than OpenSSL, if that's possible
from OpenSSL import crypto
self.installer.new_crt_files = {}
self.installer._fall_back_cert()
key = list(self.installer.new_crt_files.keys())[0]
cert = self.installer.new_crt_files[key]
self.assertIsInstance(key, str)
self.assertIsInstance(cert, str)
privkey = crypto.load_privatekey(crypto.FILETYPE_PEM, cert)
certificate = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(privkey.check())
def test_deploy_cert_save(self):
"""Deploy and save a certificate and rollback after that"""
# Variables for test:
crt_dir = os.path.join(self.temp_dir, self.test_dir, "deploy_test")
base = os.path.join(self.temp_dir, self.test_dir, "deploy_cert")
key_path = os.path.join(base, "privkey.pem")
cert_path = os.path.join(base, "cert.pem")
chain_path = os.path.join(base, "chain.pem")
fullchain_path = os.path.join(base, "fullchain.pem")
# Prepare installer
self.installer.config.no_fall_back_cert = True
self.installer.config.haproxy_crt_dir = crt_dir
# Try with files that don't exist, should raise PluginError:
self.assertRaises(
errors.PluginError,
self.installer.deploy_cert,
self.test_domain, 'no-cert', 'no-key')
# Arguments for several tests
all_args = [
(self.test_domain, cert_path, key_path),
(self.test_domain, cert_path, key_path, chain_path),
(self.test_domain, None, key_path, None, fullchain_path),
]
# Run deploy and save with all types of args
for args in all_args:
# Deploy with only key and cert
self.installer.deploy_cert(*args)
try:
self.installer.view_config_changes()
except ReverterError:
self.fail("Reverter failed")
except PluginError:
self.fail("Reverter failed with PluginError")
self.installer.save()
# Check if le.wtf.pem is created
pem = os.path.join(crt_dir, self.test_domain) \
+ self.installer.crt_postfix
self.assertTrue(os.path.isfile(pem))
# Roll back pem creation
self.installer.rollback_checkpoints()
# Check if file was removed again
self.assertFalse(os.path.isfile(pem))
# Try to revert:
try:
self.installer.recovery_routine()
except PluginError:
self.fail("Recovery routine didn't work")
# fail without key
self.assertRaises(
errors.PluginError,
self.installer.deploy_cert,
self.test_domain, cert_path, None)
# Run twice (should update instead of create)
args = (self.test_domain, cert_path, key_path)
self.installer.deploy_cert(*args)
self.installer.save()
self.installer.deploy_cert(*args)
self.installer.save()
def test_enhancement(self):
""" Currently no enhancements are supported, we should see that """
self.assertRaises(
errors.PluginError,
self.installer.enhance,
self.test_domain,
"non-existent-enhancement")
@mock.patch("certbot_haproxy.installer.logger")
@mock.patch("certbot.util.logger")
def test_config_test(self, util_logger, certbot_logger):
"""Test config_test function with a faulty and a valid cfg file"""
# Check with bad config file
self.installer.config.haproxy_config = os.path.join(
self.temp_dir, self.test_dir, "haproxy_bad.cfg")
self.assertRaises(
errors.MisconfigurationError,
self.installer.config_test
)
# Check with empty config file
self.installer.config.haproxy_config = os.path.join(
self.temp_dir, self.test_dir, "haproxy_empty.cfg")
self.assertRaises(
errors.MisconfigurationError,
self.installer.config_test
)
def test_more_info(self):
ret = self.installer.more_info()
self.assertIsInstance(ret, basestring)
@mock.patch('certbot.util.exe_exists', return_value=False)
def test_failed_service_command(self, mock_exe_exists):
""" Fail on service manager command """
self.assertRaises(errors.NoInstallationError, self.installer.prepare)
mock_exe_exists.assert_called_once()
@mock.patch('subprocess.check_output',
return_value='not-really-a-version-number')
def test_no_version_number(self, mock_check_output):
""" Fail on version command """
self.assertRaises(errors.NoInstallationError, self.installer.prepare)
@mock.patch('subprocess.check_output',
return_value='HA-Proxy version 1.4.8 2014/10/31')
def test_wrong_version_number(self, mock_check_output):
""" Supply a too low version number for HAproxy """
self.assertRaises(errors.NotSupportedError, self.installer.prepare)
mock_check_output.assert_called_once()
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHdX4LsYw2HEHC
mSWgyLkSFKHVBbtUeyWfnJk/oe62dxIJQxqPW/xaQ2TJeSvxTmoLRaTYUySn9nH2
oJ48PNcusZF86KlY/HGajMwh/9xZOPOi9iBaxzkrgzKWc9XEff4cM5v0lKWDMQn1
L/N7ilpdEBlpqlZOKOhHV3QVB5TK6ccH69AwUZnFatJRTs6PZGd4B0VKRQoIXXSV
CYtC4mhICa1mD9T0/A2eAUc4Ns5srtuUxpM5OWhslbM42NYLOfKCqaWZaHirjH67
bI7t5yCbzxrqjExtniNuXR7mbyPg3p9IVhg5xBLJU7vP0zBF2DsTdoUIuu+7Jaal
rRT5nBX5AgMBAAECggEBAK0Q7GJ3FwLod8LDIZwmLjdbAaSVyZqfE+EvfuVwu3FF
VbgFqqkTlp43dd8/LXq+oFLRhhxR4C+BLhdh/Iql+8w/NM7eDfrxaB1C5+jkWixs
tzICP8aksJ1pxF45ehV5gSC1Qy/wPsDbrxk5FxptzMSNMI+3xhUhd5JXdpi8sEVz
EpvxHUJpfgel+0ZNQ64lx0KInAmuj5rzEQG8qTUY6GRJVS8JMKdig2N7zkgRMGSG
Fphu8EN6dfgpG7bkQ6QKYao0nHNuqROaaqC7/owikyB47QKSBQgadi18HJ4F+swH
/OjWo3q3LEeaLJQj6EkXWyTuyuNt3zgdMHFASBAMSwECgYEA/Sdrkd3gZsTcsBPv
jqtFWXUDOjpoyk35naPk38zIbtdi7uFxaSvoItqMKjngakHNmAeHdKg4WOaKdrw1
7VvfJAXtPKOooz6DGLrEbT5yBqhWHNXuwGU9wVX5usLc1Z4a/3I/rJBD+E+xhGMo
I9dQJkyZuu/+ba40t0L+CXYnbNkCgYEAybOJjW1ImD1zxWHpVzRcZnDmBaPvD6Cp
xa2YB+0/e19rWmhohprqJ25VuP5bnITaQP8v33PZ5GWvF5bgwzNsDNcdXBkNfAkC
8RMEZYTvv+wLVVIHsukWDRvbn2kE/NgUTrjU8rjDbLnGSOrbxB9V/x8U8BUj8xRf
NyTFQFruviECgYActEaPMJBMNtArKueDzsm7PfhStuLA5ZyDEl1WPvsPDl8KZQed
/DuDEDLKauuWQMXWEix6vsQ82E8k7WxT8JHZnM5sjMYHVIQ63pp9JJJ7TMUO2fcL
W69n5568XXH6ysWW51Zu86CxXB/tueyIO9a7sP3RY3r+5rzVdsQlQH9x6QKBgCan
0eTY3ODQZi+d5CpiTpMTcAIEuPw1vlO0QT7DqTbrwpdBC0dAyT9lVehuVwBYNbvf
LD2W5ltvYW8T6199q2eKkoXkB0GOOJIbIuyR3aDajBF99xvCsAczH5QdfGc4wiuU
ILujzVA5CldPK28a0lBilJCufIVJkjXvRh5fTwIBAoGANCEACxXQffi5Eqsp71pW
Aa310TpVVN+Z/KcCdZsi1fE+4z7RUBg6QKX51hALO7G874/yTg+kqXg/9WJ8UprQ
KY+RLr2aKnSWyrCC+JjvjQ94Z47Ifbzx2A1fC2bY989TfqcgM/aa522nQnnY7fNw
qQu7w9gg2IIvt5PCsCkWvLk=
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDezCCAmMCAgfAMA0GCSqGSIb3DQEBCwUAMIGCMRowGAYDVQQLDBFNaW5pc3Ry
eSBvZiBUcnV0aDEaMBgGA1UECgwRTWluaXN0cnkgb2YgVHJ1dGgxGDAWBgNVBAMM
D2xlaGFwcm94eS5sb2NhbDEQMA4GA1UECAwHT2NlYW5pYTELMAkGA1UEBhMCRlUx
DzANBgNVBAcMBkxvbmRvbjAeFw0xNjA4MTcxNjUzNDVaFw0yNjA4MTUxNjUzNDVa
MIGCMRowGAYDVQQLDBFNaW5pc3RyeSBvZiBUcnV0aDEaMBgGA1UECgwRTWluaXN0
cnkgb2YgVHJ1dGgxGDAWBgNVBAMMD2xlaGFwcm94eS5sb2NhbDEQMA4GA1UECAwH
T2NlYW5pYTELMAkGA1UEBhMCRlUxDzANBgNVBAcMBkxvbmRvbjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAMd1fguxjDYcQcKZJaDIuRIUodUFu1R7JZ+c
mT+h7rZ3EglDGo9b/FpDZMl5K/FOagtFpNhTJKf2cfagnjw81y6xkXzoqVj8cZqM
zCH/3Fk486L2IFrHOSuDMpZz1cR9/hwzm/SUpYMxCfUv83uKWl0QGWmqVk4o6EdX
dBUHlMrpxwfr0DBRmcVq0lFOzo9kZ3gHRUpFCghddJUJi0LiaEgJrWYP1PT8DZ4B
Rzg2zmyu25TGkzk5aGyVszjY1gs58oKppZloeKuMfrtsju3nIJvPGuqMTG2eI25d
HuZvI+Den0hWGDnEEslTu8/TMEXYOxN2hQi677slpqWtFPmcFfkCAwEAATANBgkq
hkiG9w0BAQsFAAOCAQEAFBIa3FNO1ZamO1/mpaA5AcxkWDx6wXZLcnua3i/XfcBG
dtwHdzPCM+1naQ5wyJafQax6gHCvXyLPszKpc7ZIilhXZk5Af+SN7rCY3OJX5UDd
7lTXwt3jk+ovwOp50Q6pHZCXw/r78j8uydfEQ6UkvfaBrZiakjw7ukKuenwYC34l
UyrMYF8QLNCLxN7R55VJ8DGwo7gJgieCc8j0n+IoA4RzUDowP7hq3nVgkAojVQco
w750L0lGh08rNu9h+jv06RjIjrn8DyqaSAezyXgCBJj1WHVCFQ14mOImY3TIzmhw
+VOYtmzUCME/5yqeinybBcWmKFW9yKfukYIvdG2mwQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEeDCCA2CgAwIBAgITAP9UrVc5DIWg3kKAjAINS7ixTzANBgkqhkiG9w0BAQsF
ADAfMR0wGwYDVQQDDBRoMnBweSBoMmNrZXIgZmFrZSBDQTAeFw0xNjA4MTcwOTQ1
MDBaFw0xNjExMTUwOTQ1MDBaMEExEDAOBgNVBAMTB2xlMi53dGYxLTArBgNVBAUT
JGZmNTRhZDU3MzkwYzg1YTBkZTQyODA4YzAyMGQ0YmI4YjE0ZjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAOtmtsh3vmeE+KfmvcXVF7r0yc5HFyvREvsZ
hzwgeXFoxyzcPh6dW0MYilnm5Kn5Plrd4x/wSbVg6+ndr55fEkCrgXlkQpWMfZnf
2RgrriftarqYgMK6aJ/vIUzZP9b8cDrL33FHBorl5/WOFBeEvjSixqlT2jc42pgO
kjRjEauNRzMUsBYjCQ1yFl6AgmhufAZiXzpwbTp/pSSHtpkk47G1eGSE/pWzNCAe
e+NjtP7w9v+xoiDpxhZb2jlaZ/ZLsnSdpv4z29f6uMfKMehk8nxpm9bZf2ve5+wN
LYTToUVyC3NEDlKmEbW+9R621J4Ncz2dWPOEeprUgw0UTfBVHBUCAwEAAaOCAYkw
ggGFMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
AwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUgVI1yBLSaseacOnbXBMwyCpPFaow
HwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkwZgYIKwYBBQUHAQEEWjBY
MCIGCCsGAQUFBzABhhZodHRwOi8vMTI3LjAuMC4xOjQwMDIvMDIGCCsGAQUFBzAC
hiZodHRwOi8vMTI3LjAuMC4xOjQwMDAvYWNtZS9pc3N1ZXItY2VydDASBgNVHREE
CzAJggdsZTIud3RmMCcGA1UdHwQgMB4wHKAaoBiGFmh0dHA6Ly9leGFtcGxlLmNv
bS9jcmwwYQYDVR0gBFowWDAIBgZngQwBAgEwTAYDKgMEMEUwIgYIKwYBBQUHAgEW
Fmh0dHA6Ly9leGFtcGxlLmNvbS9jcHMwHwYIKwYBBQUHAgIwEwwRRG8gV2hhdCBU
aG91IFdpbHQwDQYJKoZIhvcNAQELBQADggEBALuEaptDgXZh+7gweuktK/6n0Ktl
/jHBIUHMTd7gDci6Gro7dOLMncSptJS/Jgmz+op/qNSR2PkY5MBl688nmzjiq143
528bdqnexfb28N9ddVcdHsL1o66f8nW+FD0quGrobvWdrRdsbMNP+QpUTiC1RjMZ
rKymQvqWsDI65ehdcUKnJkJBruibMJY7+HPgZmkbJY+CS2T9K0ATdTtendpe8vER
2eLO4wuE5lYUMy4DPtaeww6YBZ3tl157p1dmGZ0PJc0QtooRv2tu954H15B4YQaP
/YPOBlQGYzu6RM1uRCBy4lNUhpuA7H7knBimqMRmcH/mK1viaR6vqBWYyFc=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEijCCA3KgAwIBAgICEk0wDQYJKoZIhvcNAQELBQAwKzEpMCcGA1UEAwwgY2Fj
a2xpbmcgY3J5cHRvZ3JhcGhlciBmYWtlIFJPT1QwHhcNMTUxMDIxMjAxMTUyWhcN
MjAxMDE5MjAxMTUyWjAfMR0wGwYDVQQDExRoYXBweSBoYWNrZXIgZmFrZSBDQTCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMIKR3maBcUSsncXYzQT13D5
Nr+Z3mLxMMh3TUdt6sACmqbJ0btRlgXfMtNLM2OU1I6a3Ju+tIZSdn2v21JBwvxU
zpZQ4zy2cimIiMQDZCQHJwzC9GZn8HaW091iz9H0Go3A7WDXwYNmsdLNRi00o14U
joaVqaPsYrZWvRKaIRqaU0hHmS0AWwQSvN/93iMIXuyiwywmkwKbWnnxCQ/gsctK
FUtcNrwEx9Wgj6KlhwDTyI1QWSBbxVYNyUgPFzKxrSmwMO0yNff7ho+QT9x5+Y/7
XE59S4Mc4ZXxcXKew/gSlN9U5mvT+D2BhDtkCupdfsZNCQWp27A+b/DmrFI9NqsC
AwEAAaOCAcIwggG+MBIGA1UdEwEB/wQIMAYBAf8CAQAwQwYDVR0eBDwwOqE4MAaC
BC5taWwwCocIAAAAAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAyBggrBgEFBQcw
AYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5jb20wOwYIKwYB
BQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMvZHN0cm9vdGNh
eDMucDdjMB8GA1UdIwQYMBaAFOmkP+6epeby1dd5YDyTpi4kjpeqMFQGA1UdIARN
MEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUHAgEWImh0dHA6
Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUwMzAxoC+gLYYr
aHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JMLmNybDAdBgNV
HQ4EFgQU+3hPEvlgFYMsnxd/NBmzLjbqQYkwDQYJKoZIhvcNAQELBQADggEBAA0Y
AeLXOklx4hhCikUUl+BdnFfn1g0W5AiQLVNIOL6PnqXu0wjnhNyhqdwnfhYMnoy4
idRh4lB6pz8Gf9pnlLd/DnWSV3gS+/I/mAl1dCkKby6H2V790e6IHmIK2KYm3jm+
U++FIdGpBdsQTSdmiX/rAyuxMDM0adMkNBwTfQmZQCz6nGHw1QcSPZMvZpsC8Skv
ekzxsjF1otOrMUPNPQvtTWrVx8GlR2qfx/4xbQa1v2frNvFBCmO59goz+jnWvfTt
j2NjwDZ7vlMBsPm16dbKYC840uvRoZjxqsdc3ChCZjqimFqlNG/xoPA8+dTicZzC
XE9ijPIcvW6y1aa3bGw=
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDrZrbId75nhPin
5r3F1Re69MnORxcr0RL7GYc8IHlxaMcs3D4enVtDGIpZ5uSp+T5a3eMf8Em1YOvp
3a+eXxJAq4F5ZEKVjH2Z39kYK64n7Wq6mIDCumif7yFM2T/W/HA6y99xRwaK5ef1
jhQXhL40osapU9o3ONqYDpI0YxGrjUczFLAWIwkNchZegIJobnwGYl86cG06f6Uk
h7aZJOOxtXhkhP6VszQgHnvjY7T+8Pb/saIg6cYWW9o5Wmf2S7J0nab+M9vX+rjH
yjHoZPJ8aZvW2X9r3ufsDS2E06FFcgtzRA5SphG1vvUettSeDXM9nVjzhHqa1IMN
FE3wVRwVAgMBAAECggEBAL/NjksjPjIs50ovORtzuIUtpSz9QcUC8rtEHquALlOY
swk7XyYkwtKVsUz1VAWmvwbc2o4QhI5ULYT9i3W4MzAcTj+x0Ir30PNKsCIAnUON
MDRfxDJmHo+KEZEDRwAN6ggOS76Jx6vvDiRHItnS4AQVSVd4qknqejoBTWGAA0rK
HwAWds7XsdJzdN0tOCj7EkEXr+5M1wcddYCIMojwd+3puQnyh1fSluz/3JCDHTme
wuVT7Pz1fs8GOgi+7arbv2hzmjr7cHiYElbjVG92PfScNCITbGfE4MuEy1Qbyrgw
R+oAsh1qVYqA0A6kIIiPIt+qdSRgnhpsLHdIFblBtFUCgYEA//B9VCR4w9YtR0VR
5nwFsFpY21w996aoPW8IrlrFQFP6dP/oE8kE4Ys4BUgMFuvmSQl6Vq/JNDkP5Pn1
H97GIyIPbl0k4GoKzff6yUwFmG4tqg1pR+jqOeQQeiKuJwJ7YM8OgO0Y6mnqSoQv
dkE3RTpn42q++61bSL9nAlMUXtMCgYEA63T60qKB4vIfR36J2UXHoZzUoGNasJVe
UBano0xpIslfwgIGQG9PugaeBFoibaBi2tpwmhDOg99uDuG+PShWYT7WXn13/qLz
1tDWdFQfRtPCgaWUo9ytCzAtkVOa9moWkh+BcnaBhfJlKGdCg9NB2SmqXTW1yIMU
5ILIhpvZ2HcCgYBxgq1zBgHxiSnntBzQxiyXvFowX8az8Vwocvg8q9FT6GrqPR0m
uT7K8T3VSdDi0Zj+x49Upv3rhWtBOIK4scrahh6l6fWS+jT0iIjXgjnxEtx50Rzi
YbCQIFAWew8WCgnCM9LQ51bnkDXy+pE6N05WC8NLn2o4oPgdWZZL96DPNQKBgCjx
WnwuDe/ssku4UTgazi/oGYEaRbbXAmNizRu0OcP2yFPwDi4+LNroM6oGEl6FuSrd
OR9RNNfxji3AS1hPH1Yhmx0rLCZ3J3d/MQthZV7ni7cHl+U2hlCfvAy9QQEASZR3
Lgc9XCBymeyAqCxMG/MrL5itskJ/qK5jn4LoruTZAoGBALzs8ySB957HpLFWq9MP
3cBnMoM9Wrvt5Xh/E5JBOg4oZIh+Hu0whwozhnN4JGLoVPMHupV+S2dUIKqzfTrI
wXV1BD5tS3tf2/YekQK0vErJtPi5of3PVD0QGT3ll3FuTtp6jxJAWyKaf7kB29/S
EMxv+7/vwDSX2ZPf5TKiscGH
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIEZjCCA06gAwIBAgITAP9tKQ2lajvk4JedigIqcbQe9TANBgkqhkiG9w0BAQsF
ADAfMR0wGwYDVQQDDBRoMnBweSBoMmNrZXIgZmFrZSBDQTAeFw0xNjA4MTcwOTQ1
MDBaFw0xNjExMTUwOTQ1MDBaMC8xLTArBgNVBAUTJGZmNmQyOTBkYTU2YTNiZTRl
MDk3OWQ4YTAyMmE3MWI0MWVmNTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBANlZKKYuJcVi97ESQWC73E4DwKU1EuL7B845SPAXroz2R/yPsIkgnLYIX4uF
xiHG18deoe8JSODVOyZOVku0/nqse4tv/dISbiOueK+1bHn9vtPShmZuH5SrqdIS
qBrKET5dxY/yFs32OBszU5m8V6sDeKIh1vuYcxUteIzY6aZCy481Zr2tli0NVc5T
03i7oJQgsHMcGAyApTTx/Ctc/yI/M7WL4oGtzpf5XDZep0brV/LO+8rUGF8FpMe2
wV+t99mIfKMM+f2rODmyHEx2t3NKDH3XEfWr7iML8hdg/FrgzhVt4G1QTWFdap03
v6DoY7drHoOeSLlCUAQdgqY5DBMCAwEAAaOCAYkwggGFMA4GA1UdDwEB/wQEAwIF
oDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAd
BgNVHQ4EFgQU4FUyT95I5r0So+6CFTd3fHsAmfQwHwYDVR0jBBgwFoAU+3hPEvlg
FYMsnxd/NBmzLjbqQYkwZgYIKwYBBQUHAQEEWjBYMCIGCCsGAQUFBzABhhZodHRw
Oi8vMTI3LjAuMC4xOjQwMDIvMDIGCCsGAQUFBzAChiZodHRwOi8vMTI3LjAuMC4x
OjQwMDAvYWNtZS9pc3N1ZXItY2VydDASBgNVHREECzAJggdsZTMud3RmMCcGA1Ud
HwQgMB4wHKAaoBiGFmh0dHA6Ly9leGFtcGxlLmNvbS9jcmwwYQYDVR0gBFowWDAI
BgZngQwBAgEwTAYDKgMEMEUwIgYIKwYBBQUHAgEWFmh0dHA6Ly9leGFtcGxlLmNv
bS9jcHMwHwYIKwYBBQUHAgIwEwwRRG8gV2hhdCBUaG91IFdpbHQwDQYJKoZIhvcN
AQELBQADggEBALoYz4ElDobFhLDEkkPUWt6N2W14dmUSg4fc9DxCymQRSRcoXyww
KWDTAINSKKdmkVY4pdDu2rvAWf2erYW2vROUUrvsggRNzTe+SlGxZ9rf573uo4yW
f66q21snoriOG8vyTsJIMW4qzE65NKBDdNXaO/Iwt+3aV1XQnrDzSjxzyfwNbasY
SXqcjUljxnOa+u6WZ3B3+0Qvg1wvNCQ30BLCj3e2TY38XPgue289iC0AhX10RdP7
am3YdfYhyPIPy0Yvva8o8Q0xqdV5W88esZGarb+FVsCiep5zTrdi6oXyCitOlGFe
lFQIlnuLL7cYssxGVs9Dk/Zup5Cid/U3lnc=