diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index 061d67e..40e985e 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -8,6 +8,7 @@ from io import BytesIO from pyas2lib.exceptions import AS2Exception +from datetime import datetime def unquote_as2name(quoted_name): @@ -185,3 +186,50 @@ def verify_certificate_chain(cert_str, trusted_certs, ignore_self_signed=True): except crypto.X509StoreContextError as e: raise AS2Exception('Partner Certificate Invalid: %s' % e.args[-1][-1]) + + +def extract_certificate_info(cert): + """ + Extract validity information from the certificate and return a dictionary. + Provide either key with certificate (private) or public certificate + :param cert: the certificate as byte string in PEM or DER format + :return: a dictionary holding certificate information: + valid_from (datetime) + valid_to (datetime) + subject (list of name, value tuples) + issuer (list of name, value tuples) + serial (int) + """ + + # initialize the cert_info dictionary + cert_info = { + 'valid_from': None, + 'valid_to': None, + 'subject': None, + 'issuer': None, + 'serial': None + } + + # get certificate to DER list + der = pem_to_der(cert) + + # iterate through the list to find the certificate + for _item in der: + try: + # load the certificate. if element is key, exception is triggered and next element is tried + certificate = crypto.load_certificate(crypto.FILETYPE_ASN1, _item) + + # on successful load, extract the various fields into the dictionary + cert_info['valid_from'] = datetime.strptime(certificate.get_notBefore().decode('utf8'), "%Y%m%d%H%M%SZ") + cert_info['valid_to'] = datetime.strptime(certificate.get_notAfter().decode('utf8'), "%Y%m%d%H%M%SZ") + cert_info['subject'] = [tuple(item.decode('utf8') for item in sets) + for sets in certificate.get_subject().get_components()] + cert_info['issuer'] = [tuple(item.decode('utf8') for item in sets) + for sets in certificate.get_issuer().get_components()] + cert_info['serial'] = certificate.get_serial_number() + break + except crypto.Error: + continue + + # return the dictionary + return cert_info diff --git a/tests/__init__.py b/tests/__init__.py index 943bbd9..e851695 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,7 +3,7 @@ import sys sys.path.insert(0, os.path.abspath('..')) -from pyas2lib import as2, exceptions +from pyas2lib import as2, exceptions, utils class Pyas2TestCase(unittest.TestCase): @@ -46,3 +46,19 @@ def setUpClass(cls): with open(os.path.join( cls.TEST_DIR, 'cert_sb2bi_public.ca'), 'rb') as fp: cls.sb2bi_public_ca = fp.read() + + with open(os.path.join( + cls.TEST_DIR, 'cert_extract_private.cer'), 'rb') as fp: + cls.private_cer = fp.read() + + with open(os.path.join( + cls.TEST_DIR, 'cert_extract_private.pem'), 'rb') as fp: + cls.private_pem = fp.read() + + with open(os.path.join( + cls.TEST_DIR, 'cert_extract_public.cer'), 'rb') as fp: + cls.public_pem = fp.read() + + with open(os.path.join( + cls.TEST_DIR, 'cert_extract_public.cer'), 'rb') as fp: + cls.public_cer = fp.read() diff --git a/tests/fixtures/cert_extract_private.cer b/tests/fixtures/cert_extract_private.cer new file mode 100644 index 0000000..d984cce Binary files /dev/null and b/tests/fixtures/cert_extract_private.cer differ diff --git a/tests/fixtures/cert_extract_private.pem b/tests/fixtures/cert_extract_private.pem new file mode 100644 index 0000000..dc5736e --- /dev/null +++ b/tests/fixtures/cert_extract_private.pem @@ -0,0 +1,49 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIgfSzwLtO4wsCAggA +MB0GCWCGSAFlAwQBKgQQfAIjuBBfqCedJyQh0fvnRASCBNDbGjojvuy02uUpmPCr +sFdlxbAbnodKqxFi7xamh2uuqpl430S7R2QEuCZJUOzjIPPfAhdleVbdU56QIztz +d+RI+/jE8tLWPGeQ0vF1IOeSZqSjPf0ETxiRqrpEJ0KurLnyeul/7nOeEwe/bQAc +kKCDR80d7MZbPQd4L/kZ0uG2VU1tpuVljpkqI778QgOgX08/CGHNKXks3c/Yhfn1 +AxRx6eg1y0gazkR9g9wsOAJ69TAjX1hoBlmSZw22nJNZtn8eRlgzWKcKluI0NKFR +V97CA8WKUb59posspV4iyLFiFE+TvO/oMz8CzAoguLHIW5lGX5tQ6HUqbxEHkMbH +7Hv+6JhQGdHVeGZppcuIeWc/gnCc2X5PWSTx+0c7vBv2FIHM0WjX/krNagzcAL/A +u/6pGv8DrLLQTCSSdCojWJhD6q1VdiXkWnI2uQDHUNFODxywfmUkGROHHF8BcNsA +4YNKtNCEkNCs3QfoRkLXyRhEL6Rb0k1woR0iz8zK3hiuBTVvu3z6kASoJZJk7isF +DbeekZB2dnrOWZs9HcJ8gNWV61nQg9q67Rf8GzK0DA30r6tFLlWwqmWSzR3QucB/ +ddLXHiq9DyBlSznowYliGo8smHH+oxliMcZ7B8AmmrKwpEHKRGxFR1LkE9PFBIKI +X07PcuZRpynIq2/W+HFSRxFBjUWm7lsykO+ciojbj2caODfKfWs/Ma2kscopghBQ +hqqRzKlsfOxZBWeiJYrqLHZDz4asiYC1gvrc8Krx8u2mZdBodo7T2jfLAtaHMYnR +JwWEhPMq6Ixhc9OnsRVH+KeolthyT0XjR19quqH9mB8oByAhR0eQcmduPoMTwknU +Lah//rckT7sNGvJwum3iGUtIE0y0GBcU/OQ94bHelYKL4kZu//mXvvy+B0eYbqjW +3C5uy6GhPjBQ6BBMuafu+tfJ/ZGOU3ZG0g/4yrspa/qN5JuDzdMeKax8Y9jQZ5Ba +ZdjCnvr5MWO6krC6evQlkmnag+IOTAfqv+mBtOgZjVS9I49s+6XzR4UNr8dAKMg5 +E53dM2gHvg5k80i6JksspONP6+m+rL0ckrB2pYkWrGUyQi/U5f8h2CCChJfLPAEl +PUuhG9Ynh7rGubFtLFe3+RvHWtnIRmg+pxW+W9HBhv26qhkkTlx/AdVeYSoPaldG +7KsJX/6qJA4EJ4v4QWyyupCYJMmTePx/i4kIFz/CxEDiUc+5BQh08+gafiDvMaNc +7uiyhBCm3/BVWb9lSem770nz1QawH3Te4fxgKUTlj2ICaQBj37QvkVawjFaawdRq +KvtwagY/B2d8kIjgRWxRbAe/DCv9cpXxdkgsOANZn4S5fn/jmmSpG6t5zQTu55TT +Fkk4uESDIjpUYs65uuNeWZTAuNEiSJON4ha4W8h0/6g3MAjNyJ7YAQgOSSMuSGqk +xdyjWRJ0zmheL2NFYxIAmlrOquMwHMkU8YJb+9jI6RkNq8szkFenIKfCMgb9yfrX +P0aGOz3AvKRlTpcIgY1TDNoGQ0pSlMjW/VmhJJEfeOjbsQdJVr+XObmggXBYv8wE +DJhQXvVSV83iuyq7rCuEEjPs5gpKkq+K5AqXmTtJzFtVMcQ/BmJdVDssPEnjNmL8 +visz5I234/hl2utyZOj4yTSiYg== +-----END ENCRYPTED PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDBDCCAewCCQC+x6S5XDiB+TANBgkqhkiG9w0BAQsFADBEMQswCQYDVQQGEwJB +VTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwIcHlhczJsaWIxDTALBgNV +BAMMBHRlc3QwHhcNMTkwNjAzMTEzMjU3WhcNMjkwNTMxMTEzMjU3WjBEMQswCQYD +VQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwIcHlhczJsaWIx +DTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/ +ialOlAPsVGq9n3cEhFHBO9G9DyZlket2gVkVk/ONF9fqgRd1uGdrhqqOw0dwjYWH +/heuKF4FbkiNGD9r8iOF2B/Wnj8iEJO0Mc5rKKKmi5e2w/84M9VVYhkpo9AGtb0q +3COtIqbp5qU7FTqyOsvTvCa13gAVVhHm8naLxCkp6MnL0om2kNK3Exv8rYQybbpe +iLkdZda/3Qo4QEvSS4EKeQsdnN6/W7Rf9GM8gpFXCKykP2tNsESHndIxXrFBPHma +qvA8llncXyUBPJtUFrhb7Q2n+dLT07TmoctOMm+B/Dw6bN7+lHMW44/9xxMCVd/i +muhYicU7rx+bU9bWPNTFAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHfyQ15A/L6A +NZjzwScbmkjnIngjSblxOeTG30Vgcm9f+4T+bLwuF6jd4F5FngkDb/9oE3N3toEk +OwRVtV4mKhiJa5Vn1KGFqDzZ8Hs6GaKaxAFpa8XqPEQx/edVyRmX2S1MFp1qEovu +ldTsYtIC3v2ZmwoxBqPf84974cdmF6j0FrnT/eaBUWCDhjn/XFpL5ZnoDS5JSw5j +E1CpLbNRi4q6fSM+VRPrr1qkGcaXduLUN58B31QizcjCEy2XjAvVSgAq8IoILt3/ +bIUOY/Wbp1BQvVEBxALc34yxWOcUbSamIm6KYSLoMpWOsZAoyuQa2rAXIwAZwejY +9RfBSn/YWSo= +-----END CERTIFICATE----- diff --git a/tests/fixtures/cert_extract_public.cer b/tests/fixtures/cert_extract_public.cer new file mode 100644 index 0000000..d984cce Binary files /dev/null and b/tests/fixtures/cert_extract_public.cer differ diff --git a/tests/fixtures/cert_extract_public.pem b/tests/fixtures/cert_extract_public.pem new file mode 100644 index 0000000..2523b15 --- /dev/null +++ b/tests/fixtures/cert_extract_public.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBDCCAewCCQC+x6S5XDiB+TANBgkqhkiG9w0BAQsFADBEMQswCQYDVQQGEwJB +VTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwIcHlhczJsaWIxDTALBgNV +BAMMBHRlc3QwHhcNMTkwNjAzMTEzMjU3WhcNMjkwNTMxMTEzMjU3WjBEMQswCQYD +VQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwIcHlhczJsaWIx +DTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/ +ialOlAPsVGq9n3cEhFHBO9G9DyZlket2gVkVk/ONF9fqgRd1uGdrhqqOw0dwjYWH +/heuKF4FbkiNGD9r8iOF2B/Wnj8iEJO0Mc5rKKKmi5e2w/84M9VVYhkpo9AGtb0q +3COtIqbp5qU7FTqyOsvTvCa13gAVVhHm8naLxCkp6MnL0om2kNK3Exv8rYQybbpe +iLkdZda/3Qo4QEvSS4EKeQsdnN6/W7Rf9GM8gpFXCKykP2tNsESHndIxXrFBPHma +qvA8llncXyUBPJtUFrhb7Q2n+dLT07TmoctOMm+B/Dw6bN7+lHMW44/9xxMCVd/i +muhYicU7rx+bU9bWPNTFAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHfyQ15A/L6A +NZjzwScbmkjnIngjSblxOeTG30Vgcm9f+4T+bLwuF6jd4F5FngkDb/9oE3N3toEk +OwRVtV4mKhiJa5Vn1KGFqDzZ8Hs6GaKaxAFpa8XqPEQx/edVyRmX2S1MFp1qEovu +ldTsYtIC3v2ZmwoxBqPf84974cdmF6j0FrnT/eaBUWCDhjn/XFpL5ZnoDS5JSw5j +E1CpLbNRi4q6fSM+VRPrr1qkGcaXduLUN58B31QizcjCEy2XjAvVSgAq8IoILt3/ +bIUOY/Wbp1BQvVEBxALc34yxWOcUbSamIm6KYSLoMpWOsZAoyuQa2rAXIwAZwejY +9RfBSn/YWSo= +-----END CERTIFICATE----- diff --git a/tests/test_advanced.py b/tests/test_advanced.py index b3e51ec..ce39ce3 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals, absolute_import, print_function -from . import Pyas2TestCase, as2 +from . import Pyas2TestCase, as2, utils import os import base64 - +import datetime class TestAdvanced(Pyas2TestCase): @@ -325,6 +325,28 @@ def test_load_private_key(self): except as2.AS2Exception as e: self.fail('Failed to load pem private key: %s' % e) + def test_extract_certificate_info(self): + """ Test case that extracts data from private and public certificates in PEM or DER format""" + + cert_info = {'valid_from': datetime.datetime(2019, 6, 3, 11, 32, 57), + 'valid_to': datetime.datetime(2029, 5, 31, 11, 32, 57), + 'subject': [('C', 'AU'), ('ST', 'Some-State'), ('O', 'pyas2lib'), ('CN', 'test')], + 'issuer': [('C', 'AU'), ('ST', 'Some-State'), ('O', 'pyas2lib'), ('CN', 'test')], + 'serial': 13747137503594840569} + cert_empty = {'valid_from': None, + 'valid_to': None, + 'subject': None, + 'issuer': None, + 'serial': None} + + # compare result of function with cert_info dict. + self.assertEqual(utils.extract_certificate_info(self.private_pem), cert_info) + self.assertEqual(utils.extract_certificate_info(self.private_cer), cert_info) + self.assertEqual(utils.extract_certificate_info(self.public_pem), cert_info) + self.assertEqual(utils.extract_certificate_info(self.public_cer), cert_info) + self.assertEqual(utils.extract_certificate_info(b''), cert_empty) + + def find_org(self, headers): return self.org