summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Kanakarakis <ivan.kanak@gmail.com>2022-04-18 23:54:10 +0300
committerIvan Kanakarakis <ivan.kanak@gmail.com>2022-04-18 23:54:10 +0300
commita92443578acc20f3c095bdd5739d2de296746bc5 (patch)
tree49bff2ff83966cddc5e11ca6a899cc82745aa4f6
parentf36b06aa2a8aeb41394139d0fa2cf20b59f41dd9 (diff)
parente68304f64988bfeccdc525302083f718ff302c72 (diff)
downloadpysaml2-a92443578acc20f3c095bdd5739d2de296746bc5.tar.gz
Merge branch 'feat-cert-load'
-rw-r--r--setup.cfg2
-rw-r--r--src/saml2/cert.py33
-rw-r--r--src/saml2/cryptography/asymmetric.py6
-rw-r--r--src/saml2/cryptography/pki.py43
-rw-r--r--src/saml2/cryptography/symmetric.py6
-rw-r--r--src/saml2/metadata.py13
-rw-r--r--src/saml2/sigver.py66
-rw-r--r--tests/extra_lines.crt19
-rw-r--r--tests/malformed.crt12
-rw-r--r--tests/test_1.derbin0 -> 549 bytes
-rw-r--r--tests/test_39_metadata.py21
-rw-r--r--tests/test_40_sigver.py21
-rw-r--r--tests/test_82_pefim.py2
-rw-r--r--tests/test_94_read_cert.py68
-rw-r--r--tests/test_chain.pem39
-rw-r--r--tests/test_chain_with_linebreaks.pem51
16 files changed, 311 insertions, 91 deletions
diff --git a/setup.cfg b/setup.cfg
index 024030ee..4d47ee35 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -48,7 +48,7 @@ scripts =
tools/parse_xsd2.py
python_requires = >=3.6, <4
install_requires =
- cryptography >= 1.4
+ cryptography >= 3.1
defusedxml
pyOpenSSL
python-dateutil
diff --git a/src/saml2/cert.py b/src/saml2/cert.py
index 68bd55e3..aeb5b662 100644
--- a/src/saml2/cert.py
+++ b/src/saml2/cert.py
@@ -5,9 +5,10 @@ import datetime
import dateutil.parser
import pytz
import six
-from OpenSSL import crypto
-from os.path import join
from os import remove
+from os.path import join
+
+from OpenSSL import crypto
import saml2.cryptography.pki
@@ -323,8 +324,7 @@ class OpenSSLWrapper(object):
cert_algorithm = cert_algorithm.decode('ascii')
cert_str = cert_str.encode('ascii')
- cert_crypto = saml2.cryptography.pki.load_pem_x509_certificate(
- cert_str)
+ cert_crypto = saml2.cryptography.pki.load_pem_x509_certificate(cert_str)
try:
crypto.verify(ca_cert, cert_crypto.signature,
@@ -335,3 +335,28 @@ class OpenSSLWrapper(object):
return False, "Certificate is incorrectly signed."
except Exception as e:
return False, "Certificate is not valid for an unknown reason. %s" % str(e)
+
+
+def read_cert_from_file(cert_file, cert_type="pem"):
+ """Read a certificate from a file.
+
+ If there are multiple certificates in the file, the first is returned.
+
+ :param cert_file: The name of the file
+ :param cert_type: The certificate type
+ :return: A base64 encoded certificate as a string or the empty string
+ """
+ if not cert_file:
+ return ""
+
+ with open(cert_file, "rb") as fp:
+ data = fp.read()
+
+ try:
+ cert = saml2.cryptography.pki.load_x509_certificate(data, cert_type)
+ pem_data = saml2.cryptography.pki.get_public_bytes_from_cert(cert)
+ except Exception as e:
+ raise CertificateError(e)
+
+ pem_data_no_headers = "".join(pem_data.splitlines()[1:-1])
+ return pem_data_no_headers
diff --git a/src/saml2/cryptography/asymmetric.py b/src/saml2/cryptography/asymmetric.py
index 8cff93af..1c8ee519 100644
--- a/src/saml2/cryptography/asymmetric.py
+++ b/src/saml2/cryptography/asymmetric.py
@@ -1,15 +1,13 @@
"""This module provides methods for asymmetric cryptography."""
-import cryptography.hazmat.backends as _backends
import cryptography.hazmat.primitives.asymmetric as _asymmetric
import cryptography.hazmat.primitives.hashes as _hashes
import cryptography.hazmat.primitives.serialization as _serialization
-def load_pem_private_key(data, password):
+def load_pem_private_key(data, password=None):
"""Load RSA PEM certificate."""
- key = _serialization.load_pem_private_key(
- data, password, _backends.default_backend())
+ key = _serialization.load_pem_private_key(data, password)
return key
diff --git a/src/saml2/cryptography/pki.py b/src/saml2/cryptography/pki.py
index 8c59fdaf..0aa6d2b8 100644
--- a/src/saml2/cryptography/pki.py
+++ b/src/saml2/cryptography/pki.py
@@ -1,9 +1,48 @@
"""This module provides methods for PKI operations."""
-import cryptography.hazmat.backends as _backends
+from logging import getLogger as get_logger
+
import cryptography.x509 as _x509
+from cryptography.hazmat.primitives.serialization import Encoding as _cryptography_encoding
+
+
+logger = get_logger(__name__)
+
+DEFAULT_CERT_TYPE = "pem"
def load_pem_x509_certificate(data):
"""Load X.509 PEM certificate."""
- return _x509.load_pem_x509_certificate(data, _backends.default_backend())
+ return _x509.load_pem_x509_certificate(data)
+
+
+def load_der_x509_certificate(data):
+ """Load X.509 DER certificate."""
+ return _x509.load_der_x509_certificate(data)
+
+
+def load_x509_certificate(data, cert_type="pem"):
+ cert_reader = _x509_loaders.get(cert_type)
+
+ if not cert_reader:
+ cert_reader = _x509_loaders.get("pem")
+ context = {
+ "message": "Unknown cert_type, falling back to default",
+ "cert_type": cert_type,
+ "default": DEFAULT_CERT_TYPE,
+ }
+ logger.warning(context)
+
+ cert = cert_reader(data)
+ return cert
+
+
+def get_public_bytes_from_cert(cert):
+ data = cert.public_bytes(_cryptography_encoding.PEM).decode()
+ return data
+
+
+_x509_loaders = {
+ "pem": load_pem_x509_certificate,
+ "der": load_der_x509_certificate,
+}
diff --git a/src/saml2/cryptography/symmetric.py b/src/saml2/cryptography/symmetric.py
index ff73641e..ce0e19ee 100644
--- a/src/saml2/cryptography/symmetric.py
+++ b/src/saml2/cryptography/symmetric.py
@@ -10,7 +10,6 @@ import logging
from warnings import warn as _warn
import cryptography.fernet as _fernet
-import cryptography.hazmat.backends as _backends
import cryptography.hazmat.primitives.ciphers as _ciphers
from .errors import SymmetricCryptographyError
@@ -158,10 +157,7 @@ class AESCipher(object):
except KeyError:
raise Exception('Unsupported chaining mode: {}'.format(cmode))
- cipher = _ciphers.Cipher(
- _ciphers.algorithms.AES(self.key),
- mode(iv),
- backend=_backends.default_backend())
+ cipher = _ciphers.Cipher(_ciphers.algorithms.AES(self.key), mode(iv))
return cipher, iv
diff --git a/src/saml2/metadata.py b/src/saml2/metadata.py
index e7ab6011..379f73fe 100644
--- a/src/saml2/metadata.py
+++ b/src/saml2/metadata.py
@@ -2,6 +2,7 @@
from saml2.algsupport import algorithm_support_in_metadata
from saml2.md import AttributeProfile
from saml2.sigver import security_context
+from saml2.cert import read_cert_from_file
from saml2.config import Config
from saml2.validate import valid_instance
from saml2.time_util import in_a_while
@@ -688,14 +689,14 @@ def entity_descriptor(confd):
enc_cert = None
if confd.cert_file is not None:
mycert = []
- mycert.append("".join(read_cert(confd.cert_file)))
+ mycert.append(read_cert_from_file(confd.cert_file))
if confd.additional_cert_files is not None:
for _cert_file in confd.additional_cert_files:
- mycert.append("".join(read_cert(_cert_file)))
+ mycert.append(read_cert_from_file(_cert_file))
if confd.encryption_keypairs is not None:
enc_cert = []
for _encryption in confd.encryption_keypairs:
- enc_cert.append("".join(read_cert(_encryption["cert_file"])))
+ enc_cert.append(read_cert_from_file(_encryption["cert_file"]))
entd = md.EntityDescriptor()
entd.entity_id = confd.entityid
@@ -844,9 +845,3 @@ def sign_entity_descriptor(edesc, ident, secc, sign_alg=None, digest_alg=None):
xmldoc = secc.sign_statement("%s" % edesc, class_name(edesc))
edesc = md.entity_descriptor_from_string(xmldoc)
return edesc, xmldoc
-
-
-def read_cert(path):
- with open(path) as fp:
- lines = fp.readlines()
- return lines[1:-1]
diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py
index af93c42d..79e23d4f 100644
--- a/src/saml2/sigver.py
+++ b/src/saml2/sigver.py
@@ -13,6 +13,7 @@ import re
import six
import sys
from uuid import uuid4 as gen_random_key
+
from time import mktime
from tempfile import NamedTemporaryFile
from subprocess import Popen
@@ -43,6 +44,8 @@ from saml2 import class_name
from saml2 import saml
from saml2 import ExtensionElement
from saml2.cert import OpenSSLWrapper
+from saml2.cert import read_cert_from_file
+from saml2.cert import CertificateError
from saml2.extension import pefim
from saml2.extension.pefim import SPCertEnc
from saml2.saml import EncryptedAssertion
@@ -108,10 +111,6 @@ class BadSignature(SigverError):
pass
-class CertificateError(SigverError):
- pass
-
-
def get_pem_wrapped_unwrapped(cert):
begin_cert = "-----BEGIN CERTIFICATE-----\n"
end_cert = "\n-----END CERTIFICATE-----\n"
@@ -120,11 +119,6 @@ def get_pem_wrapped_unwrapped(cert):
return wrapped_cert, unwrapped_cert
-def read_file(*args, **kwargs):
- with open(*args, **kwargs) as handler:
- return handler.read()
-
-
def rm_xmltag(statement):
XMLTAG = "<?xml version='1.0'?>"
PREFIX1 = "<?xml version='1.0' encoding='UTF-8'?>"
@@ -489,8 +483,9 @@ def pem_format(key):
def import_rsa_key_from_file(filename):
- data = read_file(filename, 'rb')
- key = saml2.cryptography.asymmetric.load_pem_private_key(data, None)
+ with open(filename, "rb") as fd:
+ data = fd.read()
+ key = saml2.cryptography.asymmetric.load_pem_private_key(data)
return key
@@ -625,55 +620,6 @@ def verify_redirect_signature(saml_msg, crypto, cert=None, sigkey=None):
return bool(signer.verify(string, _sign, _key))
-def make_str(txt):
- if isinstance(txt, six.string_types):
- return txt
- else:
- return txt.decode()
-
-
-def read_cert_from_file(cert_file, cert_type):
- """ Reads a certificate from a file. The assumption is that there is
- only one certificate in the file
-
- :param cert_file: The name of the file
- :param cert_type: The certificate type
- :return: A base64 encoded certificate as a string or the empty string
- """
-
- if not cert_file:
- return ''
-
- if cert_type == 'pem':
- _a = read_file(cert_file, 'rb').decode()
- _b = _a.replace('\r\n', '\n')
- lines = _b.split('\n')
-
- for pattern in (
- '-----BEGIN CERTIFICATE-----',
- '-----BEGIN PUBLIC KEY-----'):
- if pattern in lines:
- lines = lines[lines.index(pattern) + 1:]
- break
- else:
- raise CertificateError('Strange beginning of PEM file')
-
- for pattern in (
- '-----END CERTIFICATE-----',
- '-----END PUBLIC KEY-----'):
- if pattern in lines:
- lines = lines[:lines.index(pattern)]
- break
- else:
- raise CertificateError('Strange end of PEM file')
- return make_str(''.join(lines).encode())
-
- if cert_type in ['der', 'cer', 'crt']:
- data = read_file(cert_file, 'rb')
- _cert = base64.b64encode(data)
- return make_str(_cert)
-
-
class CryptoBackend(object):
def version(self):
raise NotImplementedError()
diff --git a/tests/extra_lines.crt b/tests/extra_lines.crt
new file mode 100644
index 00000000..05b68bef
--- /dev/null
+++ b/tests/extra_lines.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIICITCCAYoCAQEwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UEBhMCenoxCzAJBgNV
+BAgMAnp6MQ0wCwYDVQQHDAR6enp6MQ4wDAYDVQQKDAVaenp6ejEOMAwGA1UECwwF
+Wnp6enoxDTALBgNVBAMMBHRlc3QwIBcNMTkwNDEyMTk1MDM0WhgPMzAxODA4MTMx
+OTUwMzRaMFgxCzAJBgNVBAYTAnp6MQswCQYDVQQIDAJ6ejENMAsGA1UEBwwEenp6
+ejEOMAwGA1UECgwFWnp6enoxDjAMBgNVBAsMBVp6enp6MQ0wCwYDVQQDDAR0ZXN0
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjW0kJM+4baWKtvO24ZsGXNvNK
+KkwTMz7OW5Z6BRqhSOq2WA0c5NCpMk6rD8Z2OTFEolPojEjf8dVyd/Ds/hrjFKQv
+8wQgbdXLN51YTIsgd6h+hBJO+vzhl0PT4aT7M0JKo5ALtS6qk4tsworW2BnwyvsG
+SAinwfeWt4t/b1J3kwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAFtj7WArQQBugmh/
+KQjjlfTQ5A052QeXfgTyO9vv1S6MRIi7qgiaEv49cGXnJv/TWbySkMKObPMUApjg
+6z8PqcxuShew5FCTkNvwhABFPiyu0fUj3e2FEPHfsBu76jz4ugtmhUqjqhzwFY9c
+tnWRkkl6J0AjM3LnHOSgjNIclDZG
+-----END CERTIFICATE-----
+
+
+
+
+
diff --git a/tests/malformed.crt b/tests/malformed.crt
new file mode 100644
index 00000000..fb4098ba
--- /dev/null
+++ b/tests/malformed.crt
@@ -0,0 +1,12 @@
+-----BEGIN CERTIFICATE-----
+MIICITCCAYoCAQEwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UEBhMCenoxCzAJBgNV
+BAgMAnp6MQ0wCwYDVQQHDAR6enp6MQ4wDAYDVQQKDAVaenp6ejEOMAwGA1UECwwF
+Wnp6enoxDTALBgNVBAMMBHRlc3QwIBcNMTkwNDEyMTk1MDM0WhgPMzAxODA4MTMx
+OTUwMzRaMFgxCzAJBgNVBAYTAnp6MQswCQYDVQQIDAJ6ejENMAsGA1UEBwwEenp6
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjW0kJM+4baWKtvO24ZsGXNvNK
+KkwTMz7OW5Z6BRqhSOq2WA0c5NCpMk6rD8Z2OTFEolPojEjf8dVyd/Ds/hrjFKQv
+8wQgbdXLN51YTIsgd6h+hBJO+vzhl0PT4aT7M0JKo5ALtS6qk4tsworW2BnwyvsG
+SAinwfeWt4t/b1J3kwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAFtj7WArQQBugmh/
+KQjjlfTQ5A052QeXfgTyO9vv1S6MRIi7qgiaEv49cGXnJv/TWbySkMKObPMUApjg
+6z8PqcxuShew5FCTkNvwhABFPiyu0fUj3e2FEPHfsBu76jz4ugtmhUqjqhzwFY9c
+tnWRkkl6J0AjM3LnHOSgjNIclDZG
diff --git a/tests/test_1.der b/tests/test_1.der
new file mode 100644
index 00000000..77abcf8d
--- /dev/null
+++ b/tests/test_1.der
Binary files differ
diff --git a/tests/test_39_metadata.py b/tests/test_39_metadata.py
index 06de507a..80f36a41 100644
--- a/tests/test_39_metadata.py
+++ b/tests/test_39_metadata.py
@@ -1,12 +1,16 @@
import copy
from saml2.config import SPConfig
-from saml2.metadata import create_metadata_string, entity_descriptor
+from saml2.metadata import create_metadata_string
+from saml2.metadata import entity_descriptor
+from saml2.cert import read_cert_from_file as read_cert
+from saml2.cert import CertificateError
from saml2.saml import NAME_FORMAT_URI, NAME_FORMAT_BASIC
from saml2 import sigver
from pathutils import full_path
-__author__ = 'roland'
+from pytest import raises
+
sp_conf = {
"entityid": "urn:mace:umu.se:saml:roland:sp",
@@ -62,5 +66,18 @@ def test_signed_metadata_proper_str_bytes_handling():
sp_metadata = create_metadata_string('', config=cnf, sign=True)
+def test_cert_trailing_newlines_ignored():
+ assert "".join(read_cert(full_path("extra_lines.crt"))) \
+ == "".join(read_cert(full_path("test_2.crt")))
+
+
+def test_invalid_cert_raises_error():
+ with raises(CertificateError):
+ read_cert(full_path("malformed.crt"))
+
+
if __name__ == '__main__':
test_requested_attribute_name_format()
+ test_cert_trailing_newlines_ignored()
+ test_invalid_cert_raises_error()
+ test_signed_metadata_proper_str_bytes_handling()
diff --git a/tests/test_40_sigver.py b/tests/test_40_sigver.py
index 87a785a1..e5225855 100644
--- a/tests/test_40_sigver.py
+++ b/tests/test_40_sigver.py
@@ -9,10 +9,11 @@ from saml2 import class_name
from saml2 import time_util
from saml2 import saml, samlp
from saml2 import config
+from saml2.cert import read_cert_from_file
+from saml2.cert import CertificateError
from saml2.sigver import pre_encryption_part
from saml2.sigver import make_temp
from saml2.sigver import XmlsecError
-from saml2.sigver import SigverError
from saml2.mdstore import MetadataStore
from saml2.saml import assertion_from_string
from saml2.saml import EncryptedAssertion
@@ -97,8 +98,7 @@ def test_cert_from_instance_1():
assert certs[0] == CERT1
-@pytest.mark.skipif(not decoder,
- reason="pyasn1 is not installed")
+@pytest.mark.skipif(not decoder, reason="pyasn1 is not installed")
def test_cert_from_instance_ssp():
with open(SIMPLE_SAML_PHP_RESPONSE) as fp:
xml_response = fp.read()
@@ -1114,6 +1114,21 @@ def test_xmlsec_output_line_parsing():
sigver.parse_xmlsec_output(output4)
+def test_cert_trailing_newlines_ignored():
+ assert read_cert_from_file(full_path("extra_lines.crt")) \
+ == read_cert_from_file(full_path("test_2.crt"))
+
+
+def test_invalid_cert_raises_error():
+ with raises(CertificateError):
+ read_cert_from_file(full_path("malformed.crt"))
+
+
+def test_der_certificate_loading():
+ assert read_cert_from_file(full_path("test_1.der"), "der") == \
+ read_cert_from_file(full_path("test_1.crt"))
+
+
if __name__ == "__main__":
# t = TestSecurity()
# t.setup_class()
diff --git a/tests/test_82_pefim.py b/tests/test_82_pefim.py
index a593d035..613a343c 100644
--- a/tests/test_82_pefim.py
+++ b/tests/test_82_pefim.py
@@ -18,7 +18,7 @@ conf.load_file("server_conf")
client = Saml2Client(conf)
# place a certificate in an authn request
-cert = read_cert_from_file(full_path("test.pem"), "pem")
+cert = read_cert_from_file(full_path("test.pem"))
spcertenc = SPCertEnc(
x509_data=ds.X509Data(
diff --git a/tests/test_94_read_cert.py b/tests/test_94_read_cert.py
new file mode 100644
index 00000000..331a7c93
--- /dev/null
+++ b/tests/test_94_read_cert.py
@@ -0,0 +1,68 @@
+from pathutils import full_path
+from saml2.cert import read_cert_from_file
+
+
+def test_read_single_cert():
+ cert = read_cert_from_file(full_path("test.pem"))
+
+ assert cert == (
+ "MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV"
+ "BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX"
+ "aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF"
+ "MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50"
+ "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB"
+ "gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy"
+ "3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN"
+ "efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G"
+ "A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs"
+ "iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt"
+ "U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw"
+ "mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6"
+ "h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5"
+ "U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6"
+ "mrPzGzk3ECbupFnqyREH3+ZPSdk="
+ )
+
+
+def test_read_cert_chain():
+ cert = read_cert_from_file(full_path("test_chain.pem"))
+
+ assert cert == (
+ "MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV"
+ "BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX"
+ "aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF"
+ "MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50"
+ "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB"
+ "gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy"
+ "3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN"
+ "efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G"
+ "A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs"
+ "iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt"
+ "U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw"
+ "mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6"
+ "h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5"
+ "U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6"
+ "mrPzGzk3ECbupFnqyREH3+ZPSdk="
+ )
+
+
+def test_read_cert_chain_with_linebreaks():
+ cert = read_cert_from_file(full_path("test_chain_with_linebreaks.pem"))
+
+ assert cert == (
+ "MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV"
+ "BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX"
+ "aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF"
+ "MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50"
+ "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB"
+ "gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy"
+ "3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN"
+ "efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G"
+ "A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs"
+ "iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt"
+ "U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw"
+ "mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6"
+ "h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5"
+ "U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6"
+ "mrPzGzk3ECbupFnqyREH3+ZPSdk="
+ )
diff --git a/tests/test_chain.pem b/tests/test_chain.pem
new file mode 100644
index 00000000..6b206e47
--- /dev/null
+++ b/tests/test_chain.pem
@@ -0,0 +1,39 @@
+-----BEGIN CERTIFICATE-----
+MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
+gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy
+3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN
+efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G
+A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs
+iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
+U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw
+mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6
+h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5
+U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6
+mrPzGzk3ECbupFnqyREH3+ZPSdk=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDnzCCAoegAwIBAgINSWITCHaai+Root+CAzANBgkqhkiG9w0BAQUFADBrMQsw
+CQYDVQQGEwJDSDFAMD4GA1UEChM3U3dpdGNoIC0gVGVsZWluZm9ybWF0aWtkaWVu
+c3RlIGZ1ZXIgTGVocmUgdW5kIEZvcnNjaHVuZzEaMBgGA1UEAxMRU1dJVENIYWFp
+IFJvb3QgQ0EwHhcNMDgwNTE1MDYzMDAwWhcNMjgwNTE1MDYyOTU5WjBrMQswCQYD
+VQQGEwJDSDFAMD4GA1UEChM3U3dpdGNoIC0gVGVsZWluZm9ybWF0aWtkaWVuc3Rl
+IGZ1ZXIgTGVocmUgdW5kIEZvcnNjaHVuZzEaMBgGA1UEAxMRU1dJVENIYWFpIFJv
+b3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUSWbn/rhWew/s
+LJRyciyRKDGyFXSgiDO/EohYuZLw6EAKLLlhZorNtEHQbbn0Oo13S33MclHMvGWT
+KJM0u1hG+6gLy78EPmJbqAE1Uv23wVEH4SX0VJfl3JVqIebiAH/CjuLubgMUspDI
+jOdQHNLS7pthTbm7Tgh7zMsiLPyMTZJep5CGbqv8NoK6bMaF0Z+Bt7e1JRlhHFCV
+iJJaR/+hfpzLsJ8NWVivvrpRGaGJ1XR+9FGsTkjNdMCirNJJZ6XvUOe5w7pHSd9M
+cppFP0eyLs02AMzMXI4iz6PK/w3EdzXGXpK+gSgvLxWYct4xHpv1e2NXhNgdJOSN
+9ra/wJLVAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG
+MB0GA1UdDgQWBBTpmuIGWOsP14EDXVyXubG1k307hDANBgkqhkiG9w0BAQUFAAOC
+AQEAMV/eIW6pFB+mbk7rD7hUPTWDRaoca3kHqmFGFnHfuY8+c0/Mqjh8Y/jyX1yb
+f58crTSWrbyGbUZ3oxDGQ34tuZSkmeR32NqryiX3sP5qlNSozVguQKt8o4vhS1Qe
+WPsXALs3em2pdKuIGSOpbuDnopPcmU2g5Zi2R5P7qpKDKAKtNUEwV+LW7GBMEksO
+Nj7BFXk4AFBFBijaYJGgHmoKSImVgeNIvsV+BSv5HJ4q6vcxfnwuvvGHM0AGphYO
+6f5qtHMUgvAblI8M/2QsBgethaGrirtKJ3aCRLdaR2R1QfaGRpck/Ron5/MpMxiJ
+wLT8YlW/zjx2yNABhPSAjfzeMw==
+-----END CERTIFICATE-----
diff --git a/tests/test_chain_with_linebreaks.pem b/tests/test_chain_with_linebreaks.pem
new file mode 100644
index 00000000..68ec8a4d
--- /dev/null
+++ b/tests/test_chain_with_linebreaks.pem
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+-----BEGIN CERTIFICATE-----
+MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
+gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy
+3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN
+efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G
+A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs
+iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
+U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw
+mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6
+h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5
+U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6
+mrPzGzk3ECbupFnqyREH3+ZPSdk=
+-----END CERTIFICATE-----
+
+
+
+-----BEGIN CERTIFICATE-----
+MIIDnzCCAoegAwIBAgINSWITCHaai+Root+CAzANBgkqhkiG9w0BAQUFADBrMQsw
+CQYDVQQGEwJDSDFAMD4GA1UEChM3U3dpdGNoIC0gVGVsZWluZm9ybWF0aWtkaWVu
+c3RlIGZ1ZXIgTGVocmUgdW5kIEZvcnNjaHVuZzEaMBgGA1UEAxMRU1dJVENIYWFp
+IFJvb3QgQ0EwHhcNMDgwNTE1MDYzMDAwWhcNMjgwNTE1MDYyOTU5WjBrMQswCQYD
+VQQGEwJDSDFAMD4GA1UEChM3U3dpdGNoIC0gVGVsZWluZm9ybWF0aWtkaWVuc3Rl
+IGZ1ZXIgTGVocmUgdW5kIEZvcnNjaHVuZzEaMBgGA1UEAxMRU1dJVENIYWFpIFJv
+b3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUSWbn/rhWew/s
+LJRyciyRKDGyFXSgiDO/EohYuZLw6EAKLLlhZorNtEHQbbn0Oo13S33MclHMvGWT
+KJM0u1hG+6gLy78EPmJbqAE1Uv23wVEH4SX0VJfl3JVqIebiAH/CjuLubgMUspDI
+jOdQHNLS7pthTbm7Tgh7zMsiLPyMTZJep5CGbqv8NoK6bMaF0Z+Bt7e1JRlhHFCV
+iJJaR/+hfpzLsJ8NWVivvrpRGaGJ1XR+9FGsTkjNdMCirNJJZ6XvUOe5w7pHSd9M
+cppFP0eyLs02AMzMXI4iz6PK/w3EdzXGXpK+gSgvLxWYct4xHpv1e2NXhNgdJOSN
+9ra/wJLVAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG
+MB0GA1UdDgQWBBTpmuIGWOsP14EDXVyXubG1k307hDANBgkqhkiG9w0BAQUFAAOC
+AQEAMV/eIW6pFB+mbk7rD7hUPTWDRaoca3kHqmFGFnHfuY8+c0/Mqjh8Y/jyX1yb
+f58crTSWrbyGbUZ3oxDGQ34tuZSkmeR32NqryiX3sP5qlNSozVguQKt8o4vhS1Qe
+WPsXALs3em2pdKuIGSOpbuDnopPcmU2g5Zi2R5P7qpKDKAKtNUEwV+LW7GBMEksO
+Nj7BFXk4AFBFBijaYJGgHmoKSImVgeNIvsV+BSv5HJ4q6vcxfnwuvvGHM0AGphYO
+6f5qtHMUgvAblI8M/2QsBgethaGrirtKJ3aCRLdaR2R1QfaGRpck/Ron5/MpMxiJ
+wLT8YlW/zjx2yNABhPSAjfzeMw==
+-----END CERTIFICATE-----
+
+
+