Merge branch 'master' into removing-outdated-language

This commit is contained in:
Hossein Shafagh 2020-10-12 10:22:53 -07:00 committed by GitHub
commit 5db1d31668
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 284 additions and 34 deletions

View File

@ -39,6 +39,22 @@ def update(authority_id, description, owner, active, roles):
return database.update(authority) return database.update(authority)
def update_options(authority_id, options):
"""
Update an authority with new options.
:param authority_id:
:param options: the new options to be saved into the authority
:return:
"""
authority = get(authority_id)
authority.options = options
return database.update(authority)
def mint(**kwargs): def mint(**kwargs):
""" """
Creates the authority based on the plugin provided. Creates the authority based on the plugin provided.

View File

@ -23,6 +23,7 @@ from lemur.domains.schemas import DomainNestedOutputSchema
from lemur.notifications import service as notification_service from lemur.notifications import service as notification_service
from lemur.notifications.schemas import NotificationNestedOutputSchema from lemur.notifications.schemas import NotificationNestedOutputSchema
from lemur.policies.schemas import RotationPolicyNestedOutputSchema from lemur.policies.schemas import RotationPolicyNestedOutputSchema
from lemur.roles import service as roles_service
from lemur.roles.schemas import RoleNestedOutputSchema from lemur.roles.schemas import RoleNestedOutputSchema
from lemur.schemas import ( from lemur.schemas import (
AssociatedAuthoritySchema, AssociatedAuthoritySchema,
@ -184,13 +185,33 @@ class CertificateEditInputSchema(CertificateSchema):
data["replaces"] = data[ data["replaces"] = data[
"replacements" "replacements"
] # TODO remove when field is deprecated ] # TODO remove when field is deprecated
if data.get("owner"):
# Check if role already exists. This avoids adding duplicate role.
if data.get("roles") and any(r.get("name") == data["owner"] for r in data["roles"]):
return data
# Add required role
owner_role = roles_service.get_or_create(
data["owner"],
description=f"Auto generated role based on owner: {data['owner']}"
)
# Put role info in correct format using RoleNestedOutputSchema
owner_role_dict = RoleNestedOutputSchema().dump(owner_role).data
if data.get("roles"):
data["roles"].append(owner_role_dict)
else:
data["roles"] = [owner_role_dict]
return data return data
@post_load @post_load
def enforce_notifications(self, data): def enforce_notifications(self, data):
""" """
Ensures that when an owner changes, default notifications are added for the new owner. Add default notification for current owner if none exist.
Old owner notifications are retained unless explicitly removed. This ensures that the default notifications are added in the event of owner change.
Old owner notifications are retained unless explicitly removed later in the code path.
:param data: :param data:
:return: :return:
""" """
@ -198,11 +219,18 @@ class CertificateEditInputSchema(CertificateSchema):
notification_name = "DEFAULT_{0}".format( notification_name = "DEFAULT_{0}".format(
data["owner"].split("@")[0].upper() data["owner"].split("@")[0].upper()
) )
# Even if one default role exists, return
# This allows a User to remove unwanted default notification for current owner
if any(n.label.startswith(notification_name) for n in data["notifications"]):
return data
data[ data[
"notifications" "notifications"
] += notification_service.create_default_expiration_notifications( ] += notification_service.create_default_expiration_notifications(
notification_name, [data["owner"]] notification_name, [data["owner"]]
) )
return data return data

View File

@ -256,6 +256,12 @@ def update(cert_id, **kwargs):
return database.update(cert) return database.update(cert)
def cleanup_owner_roles_notification(owner_name, kwargs):
kwargs["roles"] = [r for r in kwargs["roles"] if r.name != owner_name]
notification_prefix = f"DEFAULT_{owner_name.split('@')[0].upper()}"
kwargs["notifications"] = [n for n in kwargs["notifications"] if not n.label.startswith(notification_prefix)]
def update_notify(cert, notify_flag): def update_notify(cert, notify_flag):
""" """
Toggle notification value which is a boolean Toggle notification value which is a boolean
@ -268,16 +274,11 @@ def update_notify(cert, notify_flag):
def create_certificate_roles(**kwargs): def create_certificate_roles(**kwargs):
# create an role for the owner and assign it # create a role for the owner and assign it
owner_role = role_service.get_by_name(kwargs["owner"]) owner_role = role_service.get_or_create(
kwargs["owner"],
if not owner_role: description=f"Auto generated role based on owner: {kwargs['owner']}"
owner_role = role_service.create( )
kwargs["owner"],
description="Auto generated role based on owner: {0}".format(
kwargs["owner"]
),
)
# ensure that the authority's owner is also associated with the certificate # ensure that the authority's owner is also associated with the certificate
if kwargs.get("authority"): if kwargs.get("authority"):

View File

@ -884,6 +884,10 @@ class Certificates(AuthenticatedResource):
400, 400,
) )
# if owner is changed, remove all notifications and roles associated with old owner
if cert.owner != data["owner"]:
service.cleanup_owner_roles_notification(cert.owner, data)
cert = service.update(certificate_id, **data) cert = service.update(certificate_id, **data)
log_service.create(g.current_user, "update_cert", certificate=cert) log_service.create(g.current_user, "update_cert", certificate=cert)
return cert return cert

View File

@ -9,6 +9,7 @@
import random import random
import re import re
import string import string
import pem
import sqlalchemy import sqlalchemy
from cryptography import x509 from cryptography import x509
@ -16,7 +17,7 @@ from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives.serialization import load_pem_private_key, Encoding, pkcs7
from flask_restful.reqparse import RequestParser from flask_restful.reqparse import RequestParser
from sqlalchemy import and_, func from sqlalchemy import and_, func
@ -357,3 +358,19 @@ def find_matching_certificates_by_hash(cert, matching_certs):
): ):
matching.append(c) matching.append(c)
return matching return matching
def convert_pkcs7_bytes_to_pem(certs_pkcs7):
"""
Given a list of certificates in pkcs7 encoding (bytes), covert them into a list of PEM encoded files
:raises ValueError or ValidationError
:param certs_pkcs7:
:return: list of certs in PEM format
"""
certificates = pkcs7.load_pem_pkcs7_certificates(certs_pkcs7)
certificates_pem = []
for cert in certificates:
certificates_pem.append(pem.parse(cert.public_bytes(encoding=Encoding.PEM))[0])
return certificates_pem

View File

@ -32,6 +32,7 @@ from lemur.extensions import metrics, sentry
from lemur.plugins import lemur_acme as acme from lemur.plugins import lemur_acme as acme
from lemur.plugins.bases import IssuerPlugin from lemur.plugins.bases import IssuerPlugin
from lemur.plugins.lemur_acme import cloudflare, dyn, route53, ultradns, powerdns from lemur.plugins.lemur_acme import cloudflare, dyn, route53, ultradns, powerdns
from lemur.authorities import service as authorities_service
from retrying import retry from retrying import retry
@ -240,6 +241,7 @@ class AcmeHandler(object):
existing_regr = options.get("acme_regr", current_app.config.get("ACME_REGR")) existing_regr = options.get("acme_regr", current_app.config.get("ACME_REGR"))
if existing_key and existing_regr: if existing_key and existing_regr:
current_app.logger.debug("Reusing existing ACME account")
# Reuse the same account for each certificate issuance # Reuse the same account for each certificate issuance
key = jose.JWK.json_loads(existing_key) key = jose.JWK.json_loads(existing_key)
regr = messages.RegistrationResource.json_loads(existing_regr) regr = messages.RegistrationResource.json_loads(existing_regr)
@ -253,6 +255,7 @@ class AcmeHandler(object):
# Create an account for each certificate issuance # Create an account for each certificate issuance
key = jose.JWKRSA(key=generate_private_key("RSA2048")) key = jose.JWKRSA(key=generate_private_key("RSA2048"))
current_app.logger.debug("Creating a new ACME account")
current_app.logger.debug( current_app.logger.debug(
"Connecting with directory at {0}".format(directory_url) "Connecting with directory at {0}".format(directory_url)
) )
@ -262,6 +265,27 @@ class AcmeHandler(object):
registration = client.new_account_and_tos( registration = client.new_account_and_tos(
messages.NewRegistration.from_data(email=email) messages.NewRegistration.from_data(email=email)
) )
# if store_account is checked, add the private_key and registration resources to the options
if options['store_account']:
new_options = json.loads(authority.options)
# the key returned by fields_to_partial_json is missing the key type, so we add it manually
key_dict = key.fields_to_partial_json()
key_dict["kty"] = "RSA"
acme_private_key = {
"name": "acme_private_key",
"value": json.dumps(key_dict)
}
new_options.append(acme_private_key)
acme_regr = {
"name": "acme_regr",
"value": json.dumps({"body": {}, "uri": registration.uri})
}
new_options.append(acme_regr)
authorities_service.update_options(authority.id, options=json.dumps(new_options))
current_app.logger.debug("Connected: {0}".format(registration.uri)) current_app.logger.debug("Connected: {0}".format(registration.uri))
return client, registration return client, registration
@ -447,6 +471,13 @@ class ACMEIssuerPlugin(IssuerPlugin):
"validation": "/^-----BEGIN CERTIFICATE-----/", "validation": "/^-----BEGIN CERTIFICATE-----/",
"helpMessage": "Certificate to use", "helpMessage": "Certificate to use",
}, },
{
"name": "store_account",
"type": "bool",
"required": False,
"helpMessage": "Disable to create a new account for each ACME request",
"default": False,
}
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -1,8 +1,10 @@
import unittest import unittest
from unittest.mock import patch, Mock from unittest.mock import patch, Mock
import josepy as jose
from cryptography.x509 import DNSName from cryptography.x509 import DNSName
from lemur.plugins.lemur_acme import plugin from lemur.plugins.lemur_acme import plugin
from lemur.common.utils import generate_private_key
from mock import MagicMock from mock import MagicMock
@ -165,11 +167,65 @@ class TestAcme(unittest.TestCase):
with self.assertRaises(Exception): with self.assertRaises(Exception):
self.acme.setup_acme_client(mock_authority) self.acme.setup_acme_client(mock_authority)
@patch("lemur.plugins.lemur_acme.plugin.jose.JWK.json_loads")
@patch("lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2") @patch("lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2")
@patch("lemur.plugins.lemur_acme.plugin.current_app") @patch("lemur.plugins.lemur_acme.plugin.current_app")
def test_setup_acme_client_success(self, mock_current_app, mock_acme): def test_setup_acme_client_success_load_account_from_authority(self, mock_current_app, mock_acme, mock_key_json_load):
mock_authority = Mock() mock_authority = Mock()
mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}]' mock_authority.id = 2
mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}, ' \
'{"name": "store_account", "value": true},' \
'{"name": "acme_private_key", "value": "{\\"n\\": \\"PwIOkViO\\", \\"kty\\": \\"RSA\\"}"}, ' \
'{"name": "acme_regr", "value": "{\\"body\\": {}, \\"uri\\": \\"http://test.com\\"}"}]'
mock_client = Mock()
mock_acme.return_value = mock_client
mock_current_app.config = {}
mock_key_json_load.return_value = jose.JWKRSA(key=generate_private_key("RSA2048"))
result_client, result_registration = self.acme.setup_acme_client(mock_authority)
mock_acme.new_account_and_tos.assert_not_called()
assert result_client
assert not result_registration
@patch("lemur.plugins.lemur_acme.plugin.jose.JWKRSA.fields_to_partial_json")
@patch("lemur.plugins.lemur_acme.plugin.authorities_service")
@patch("lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2")
@patch("lemur.plugins.lemur_acme.plugin.current_app")
def test_setup_acme_client_success_store_new_account(self, mock_current_app, mock_acme, mock_authorities_service,
mock_key_generation):
mock_authority = Mock()
mock_authority.id = 2
mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}, ' \
'{"name": "store_account", "value": true}]'
mock_client = Mock()
mock_registration = Mock()
mock_registration.uri = "http://test.com"
mock_client.register = mock_registration
mock_client.agree_to_tos = Mock(return_value=True)
mock_client.new_account_and_tos.return_value = mock_registration
mock_acme.return_value = mock_client
mock_current_app.config = {}
mock_key_generation.return_value = {"n": "PwIOkViO"}
mock_authorities_service.update_options = Mock(return_value=True)
self.acme.setup_acme_client(mock_authority)
mock_authorities_service.update_options.assert_called_with(2, options='[{"name": "mock_name", "value": "mock_value"}, '
'{"name": "store_account", "value": true}, '
'{"name": "acme_private_key", "value": "{\\"n\\": \\"PwIOkViO\\", \\"kty\\": \\"RSA\\"}"}, '
'{"name": "acme_regr", "value": "{\\"body\\": {}, \\"uri\\": \\"http://test.com\\"}"}]')
@patch("lemur.plugins.lemur_acme.plugin.authorities_service")
@patch("lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2")
@patch("lemur.plugins.lemur_acme.plugin.current_app")
def test_setup_acme_client_success(self, mock_current_app, mock_acme, mock_authorities_service):
mock_authority = Mock()
mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}, ' \
'{"name": "store_account", "value": false}]'
mock_client = Mock() mock_client = Mock()
mock_registration = Mock() mock_registration = Mock()
mock_registration.uri = "http://test.com" mock_registration.uri = "http://test.com"
@ -178,6 +234,7 @@ class TestAcme(unittest.TestCase):
mock_acme.return_value = mock_client mock_acme.return_value = mock_client
mock_current_app.config = {} mock_current_app.config = {}
result_client, result_registration = self.acme.setup_acme_client(mock_authority) result_client, result_registration = self.acme.setup_acme_client(mock_authority)
mock_authorities_service.update_options.assert_not_called()
assert result_client assert result_client
assert result_registration assert result_registration

View File

@ -21,7 +21,7 @@ import requests
import sys import sys
from cryptography import x509 from cryptography import x509
from flask import current_app, g from flask import current_app, g
from lemur.common.utils import validate_conf from lemur.common.utils import validate_conf, convert_pkcs7_bytes_to_pem
from lemur.extensions import metrics from lemur.extensions import metrics
from lemur.plugins import lemur_digicert as digicert from lemur.plugins import lemur_digicert as digicert
from lemur.plugins.bases import IssuerPlugin, SourcePlugin from lemur.plugins.bases import IssuerPlugin, SourcePlugin
@ -235,15 +235,18 @@ def get_certificate_id(session, base_url, order_id):
@retry(stop_max_attempt_number=10, wait_fixed=10000) @retry(stop_max_attempt_number=10, wait_fixed=10000)
def get_cis_certificate(session, base_url, order_id): def get_cis_certificate(session, base_url, order_id):
"""Retrieve certificate order id from Digicert API.""" """Retrieve certificate order id from Digicert API, including the chain"""
certificate_url = "{0}/platform/cis/certificate/{1}".format(base_url, order_id) certificate_url = "{0}/platform/cis/certificate/{1}/download".format(base_url, order_id)
session.headers.update({"Accept": "application/x-pem-file"}) session.headers.update({"Accept": "application/x-pkcs7-certificates"})
response = session.get(certificate_url) response = session.get(certificate_url)
if response.status_code == 404: if response.status_code == 404:
raise Exception("Order not in issued state.") raise Exception("Order not in issued state.")
return response.content cert_chain_pem = convert_pkcs7_bytes_to_pem(response.content)
if len(cert_chain_pem) < 3:
raise Exception("Missing the certificate chain")
return cert_chain_pem
class DigiCertSourcePlugin(SourcePlugin): class DigiCertSourcePlugin(SourcePlugin):
@ -447,7 +450,6 @@ class DigiCertCISSourcePlugin(SourcePlugin):
"DIGICERT_CIS_API_KEY", "DIGICERT_CIS_API_KEY",
"DIGICERT_CIS_URL", "DIGICERT_CIS_URL",
"DIGICERT_CIS_ROOTS", "DIGICERT_CIS_ROOTS",
"DIGICERT_CIS_INTERMEDIATES",
"DIGICERT_CIS_PROFILE_NAMES", "DIGICERT_CIS_PROFILE_NAMES",
] ]
validate_conf(current_app, required_vars) validate_conf(current_app, required_vars)
@ -522,7 +524,6 @@ class DigiCertCISIssuerPlugin(IssuerPlugin):
"DIGICERT_CIS_API_KEY", "DIGICERT_CIS_API_KEY",
"DIGICERT_CIS_URL", "DIGICERT_CIS_URL",
"DIGICERT_CIS_ROOTS", "DIGICERT_CIS_ROOTS",
"DIGICERT_CIS_INTERMEDIATES",
"DIGICERT_CIS_PROFILE_NAMES", "DIGICERT_CIS_PROFILE_NAMES",
] ]
@ -552,22 +553,15 @@ class DigiCertCISIssuerPlugin(IssuerPlugin):
data = handle_cis_response(response) data = handle_cis_response(response)
# retrieve certificate # retrieve certificate
certificate_pem = get_cis_certificate(self.session, base_url, data["id"]) certificate_chain_pem = get_cis_certificate(self.session, base_url, data["id"])
self.session.headers.pop("Accept") self.session.headers.pop("Accept")
end_entity = pem.parse(certificate_pem)[0] end_entity = certificate_chain_pem[0]
intermediate = certificate_chain_pem[1]
if "ECC" in issuer_options["key_type"]:
return (
"\n".join(str(end_entity).splitlines()),
current_app.config.get("DIGICERT_ECC_CIS_INTERMEDIATES", {}).get(issuer_options['authority'].name),
data["id"],
)
# By default return RSA
return ( return (
"\n".join(str(end_entity).splitlines()), "\n".join(str(end_entity).splitlines()),
current_app.config.get("DIGICERT_CIS_INTERMEDIATES", {}).get(issuer_options['authority'].name), "\n".join(str(intermediate).splitlines()),
data["id"], data["id"],
) )

View File

@ -128,3 +128,11 @@ def render(args):
query = database.filter(query, Role, terms) query = database.filter(query, Role, terms)
return database.sort_and_page(query, Role, args) return database.sort_and_page(query, Role, args)
def get_or_create(role_name, description):
role = get_by_name(role_name)
if not role:
role = create(name=role_name, description=description)
return role

View File

@ -99,7 +99,6 @@ DIGICERT_CIS_URL = "mock://www.digicert.com"
DIGICERT_CIS_PROFILE_NAMES = {"sha2-rsa-ecc-root": "ssl_plus"} DIGICERT_CIS_PROFILE_NAMES = {"sha2-rsa-ecc-root": "ssl_plus"}
DIGICERT_CIS_API_KEY = "api-key" DIGICERT_CIS_API_KEY = "api-key"
DIGICERT_CIS_ROOTS = {"root": "ROOT"} DIGICERT_CIS_ROOTS = {"root": "ROOT"}
DIGICERT_CIS_INTERMEDIATES = {"inter": "INTERMEDIATE_CA_CERT"}
VERISIGN_URL = "http://example.com" VERISIGN_URL = "http://example.com"
VERISIGN_PEM_PATH = "~/" VERISIGN_PEM_PATH = "~/"

View File

@ -180,7 +180,10 @@ def test_certificate_edit_schema(session):
input_data = {"owner": "bob@example.com"} input_data = {"owner": "bob@example.com"}
data, errors = CertificateEditInputSchema().load(input_data) data, errors = CertificateEditInputSchema().load(input_data)
assert not errors
assert len(data["notifications"]) == 3 assert len(data["notifications"]) == 3
assert data["roles"][0].name == input_data["owner"]
def test_authority_key_identifier_schema(): def test_authority_key_identifier_schema():
@ -970,6 +973,9 @@ def test_certificate_put_with_data(client, certificate, issuer_plugin):
headers=VALID_ADMIN_HEADER_TOKEN, headers=VALID_ADMIN_HEADER_TOKEN,
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert len(certificate.notifications) == 3
assert certificate.roles[0].name == "bob@example.com"
assert certificate.notify
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -10,6 +10,7 @@ from lemur.tests.vectors import (
ECDSA_SECP384r1_CERT, ECDSA_SECP384r1_CERT,
ECDSA_SECP384r1_CERT_STR, ECDSA_SECP384r1_CERT_STR,
DSA_CERT, DSA_CERT,
CERT_CHAIN_PKCS7_PEM
) )
@ -114,3 +115,16 @@ def test_get_key_type_from_certificate():
from lemur.common.utils import get_key_type_from_certificate from lemur.common.utils import get_key_type_from_certificate
assert (get_key_type_from_certificate(SAN_CERT_STR) == "RSA2048") assert (get_key_type_from_certificate(SAN_CERT_STR) == "RSA2048")
assert (get_key_type_from_certificate(ECDSA_SECP384r1_CERT_STR) == "ECCSECP384R1") assert (get_key_type_from_certificate(ECDSA_SECP384r1_CERT_STR) == "ECCSECP384R1")
def test_convert_pkcs7_bytes_to_pem():
from lemur.common.utils import convert_pkcs7_bytes_to_pem
from lemur.common.utils import parse_certificate
cert_chain = convert_pkcs7_bytes_to_pem(CERT_CHAIN_PKCS7_PEM)
assert(len(cert_chain) == 3)
leaf = cert_chain[1]
root = cert_chain[2]
assert(parse_certificate("\n".join(str(root).splitlines())) == ROOTCA_CERT)
assert (parse_certificate("\n".join(str(leaf).splitlines())) == INTERMEDIATE_CERT)

View File

@ -512,3 +512,78 @@ BglghkgBZQMEAwIDMAAwLQIVANubSNMSLt8plN9ZV3cp4pe3lMYCAhQPLLE7rTgm
-----END CERTIFICATE----- -----END CERTIFICATE-----
""" """
DSA_CERT = parse_certificate(DSA_CERT_STR) DSA_CERT = parse_certificate(DSA_CERT_STR)
CERT_CHAIN_PKCS7_STR = """
-----BEGIN PKCS7-----
MIIMfwYJKoZIhvcNAQcCoIIMcDCCDGwCAQExADALBgkqhkiG9w0BBwGgggxSMIIE
FjCCAv6gAwIBAgIQbIbX/Ap0Roqzf5HeN5akmzANBgkqhkiG9w0BAQsFADCBpDEq
MCgGA1UEAwwhTGVtdXJUcnVzdCBVbml0dGVzdHMgUm9vdCBDQSAyMDE4MSMwIQYD
VQQKDBpMZW11clRydXN0IEVudGVycHJpc2VzIEx0ZDEmMCQGA1UECwwdVW5pdHRl
c3RpbmcgT3BlcmF0aW9ucyBDZW50ZXIxCzAJBgNVBAYTAkVFMQwwCgYDVQQIDANO
L0ExDjAMBgNVBAcMBUVhcnRoMB4XDTE3MTIzMTIyMDAwMFoXDTQ3MTIzMTIyMDAw
MFowgaQxKjAoBgNVBAMMIUxlbXVyVHJ1c3QgVW5pdHRlc3RzIFJvb3QgQ0EgMjAx
ODEjMCEGA1UECgwaTGVtdXJUcnVzdCBFbnRlcnByaXNlcyBMdGQxJjAkBgNVBAsM
HVVuaXR0ZXN0aW5nIE9wZXJhdGlvbnMgQ2VudGVyMQswCQYDVQQGEwJFRTEMMAoG
A1UECAwDTi9BMQ4wDAYDVQQHDAVFYXJ0aDCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAL8laXtLXyM64t5dz2B9q+4VvOsChefBi2PlGudqxDuRN3l0Kmcf
un6x2Gng24pTlGdtmiTEWA0a2F8HRLv4YBWhuYleVeBPtf1fF1/SuYgkJOWT7S5q
k/od/tUOLHS0Y067st3FydnFQTKpAuYveEkxleFrMS8hX8cuEgbER+8ybiXKn4Gs
yM/om6lsTyBoaLp5yTAoQb4jAWDbiz1xcjPSkvH2lm7rLGtKoylCYwxRsMh2nZcR
r1OXVhYHXwpYHVB/jVAjy7PAWQ316hi6mpPYbBV+yfn2GUfGuytqyoXLEsrM3iEE
AkU0mJjQmYsCDM3r7ONHTM+UFEk47HCZJccCAwEAAaNCMEAwDwYDVR0TAQH/BAUw
AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFL12SFeOTTDdGKsHKozeByG
HY6nMA0GCSqGSIb3DQEBCwUAA4IBAQAJfe0/uAHobkxth38dqrSFmTo+D5/TMlRt
3hdgjlah6sD2+/DObCyut/XhQWCgTNWyRi4xTKgLh5KSoeJ9EMkADGEgDkU2vjBg
5FmGZsxg6bqjxehK+2HvASJoTH8r41xmTioav7a2i3wNhaNSntw2QRTQBQEDOIzH
RpPDQ2quErjA8nSifE2xmAAr3g+FuookTTJuv37s2cS59zRYsg+WC3+TtPpRssvo
bJ6Xe2D4cCVjUmsqtFEztMgdqgmlcWyGdUKeXdi7CMoeTb4uO+9qRQq46wYWn7K1
z+W0Kp5yhnnPAoOioAP4vjASDx3z3RnLaZvMmcO7YdCIwhE5oGV0MIIEGjCCAwKg
AwIBAgIRAJ96dbOdrkw/lSTGiwbaagwwDQYJKoZIhvcNAQELBQAwgaQxKjAoBgNV
BAMMIUxlbXVyVHJ1c3QgVW5pdHRlc3RzIFJvb3QgQ0EgMjAxODEjMCEGA1UECgwa
TGVtdXJUcnVzdCBFbnRlcnByaXNlcyBMdGQxJjAkBgNVBAsMHVVuaXR0ZXN0aW5n
IE9wZXJhdGlvbnMgQ2VudGVyMQswCQYDVQQGEwJFRTEMMAoGA1UECAwDTi9BMQ4w
DAYDVQQHDAVFYXJ0aDAeFw0xNzEyMzEyMjAwMDBaFw00NzEyMzEyMjAwMDBaMIGn
MS0wKwYDVQQDDCRMZW11clRydXN0IFVuaXR0ZXN0cyBDbGFzcyAxIENBIDIwMTgx
IzAhBgNVBAoMGkxlbXVyVHJ1c3QgRW50ZXJwcmlzZXMgTHRkMSYwJAYDVQQLDB1V
bml0dGVzdGluZyBPcGVyYXRpb25zIENlbnRlcjELMAkGA1UEBhMCRUUxDDAKBgNV
BAgMA04vQTEOMAwGA1UEBwwFRWFydGgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQDR+qNdfNsLhGvgw3IgCQNakL2B9dpQtkVnvAXhdRZqJETm/tHLkGvO
NWTXAwGdoiKv6+0j3I5InUsW+wzUPewcfj+PLNu4mFMq8jH/gPhTElKiAztPRdm8
QKchvrqiaU6uEbia8ClM6uPpIi8StxE1aJRYL03p0WeMJjJPrsl6eSSdpR4qL69G
Td1n5je9OuWAcn5utXXnt/jO4vNeFRjlGp/0n3JmTDd9w4vtAyY9UrdGgo37eBmi
6mXt5J9i//NenhaiOVU81RqxZM2Jt1kkg2WSjcqcIQfBEWp9StG46VmHLaL+9/v2
XAV3tL1VilJGj6PoFMb4gY5MXthfGSiXAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB
Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQstpQr0iMBVfv0lODIsMgT9+9o
ezANBgkqhkiG9w0BAQsFAAOCAQEASYQbv1Qwb5zES6Gb5LEhrAcH81ZB2uIpKd3K
i6AS4fLJVymMGkUs0RZjt39Ep4qX1zf0hn82Yh9YwRalrkgu+tzKrp0JgegNe6+g
yFRrJC0SIGA4zc3M02m/n4tdaouU2lp6jhmWruL3g25ZkgbQ8LO2zjpSMtblR2eu
vR2+bI7TepklyG71qx5y6/N8x5PT+hnTlleiZeE/ji9D96MZlpWB4kBihekWmxup
tED22z/tpQtac+hPBNgt8z1uFVEYN2rKEcCE7V6Qk7icS+M4Vb7M3D8kLyWDubs9
Yy3l0EWjOXQXxEhTaKEm4gSuY/j+Y35bBVkA2Fcyuq7msiTgrzCCBBYwggL+oAMC
AQICEGyG1/wKdEaKs3+R3jeWpJswDQYJKoZIhvcNAQELBQAwgaQxKjAoBgNVBAMM
IUxlbXVyVHJ1c3QgVW5pdHRlc3RzIFJvb3QgQ0EgMjAxODEjMCEGA1UECgwaTGVt
dXJUcnVzdCBFbnRlcnByaXNlcyBMdGQxJjAkBgNVBAsMHVVuaXR0ZXN0aW5nIE9w
ZXJhdGlvbnMgQ2VudGVyMQswCQYDVQQGEwJFRTEMMAoGA1UECAwDTi9BMQ4wDAYD
VQQHDAVFYXJ0aDAeFw0xNzEyMzEyMjAwMDBaFw00NzEyMzEyMjAwMDBaMIGkMSow
KAYDVQQDDCFMZW11clRydXN0IFVuaXR0ZXN0cyBSb290IENBIDIwMTgxIzAhBgNV
BAoMGkxlbXVyVHJ1c3QgRW50ZXJwcmlzZXMgTHRkMSYwJAYDVQQLDB1Vbml0dGVz
dGluZyBPcGVyYXRpb25zIENlbnRlcjELMAkGA1UEBhMCRUUxDDAKBgNVBAgMA04v
QTEOMAwGA1UEBwwFRWFydGgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQC/JWl7S18jOuLeXc9gfavuFbzrAoXnwYtj5RrnasQ7kTd5dCpnH7p+sdhp4NuK
U5RnbZokxFgNGthfB0S7+GAVobmJXlXgT7X9Xxdf0rmIJCTlk+0uapP6Hf7VDix0
tGNOu7LdxcnZxUEyqQLmL3hJMZXhazEvIV/HLhIGxEfvMm4lyp+BrMjP6JupbE8g
aGi6eckwKEG+IwFg24s9cXIz0pLx9pZu6yxrSqMpQmMMUbDIdp2XEa9Tl1YWB18K
WB1Qf41QI8uzwFkN9eoYupqT2GwVfsn59hlHxrsrasqFyxLKzN4hBAJFNJiY0JmL
AgzN6+zjR0zPlBRJOOxwmSXHAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRS9dkhXjk0w3RirByqM3gchh2OpzANBgkq
hkiG9w0BAQsFAAOCAQEACX3tP7gB6G5MbYd/Haq0hZk6Pg+f0zJUbd4XYI5WoerA
9vvwzmwsrrf14UFgoEzVskYuMUyoC4eSkqHifRDJAAxhIA5FNr4wYORZhmbMYOm6
o8XoSvth7wEiaEx/K+NcZk4qGr+2tot8DYWjUp7cNkEU0AUBAziMx0aTw0NqrhK4
wPJ0onxNsZgAK94PhbqKJE0ybr9+7NnEufc0WLIPlgt/k7T6UbLL6Gyel3tg+HAl
Y1JrKrRRM7TIHaoJpXFshnVCnl3YuwjKHk2+LjvvakUKuOsGFp+ytc/ltCqecoZ5
zwKDoqAD+L4wEg8d890Zy2mbzJnDu2HQiMIROaBldKEAMQA=
-----END PKCS7-----
"""
CERT_CHAIN_PKCS7_PEM = CERT_CHAIN_PKCS7_STR.encode('utf-8')