From f7fbf8a96696fa227f5d2500d0864d83d6184656 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Thu, 15 Feb 2018 12:41:09 +0530 Subject: [PATCH 01/66] add coverage --- .travis.yml | 6 +++++- README.md | 3 +++ setup.py | 7 ++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 692f2df..f6dae0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,5 +6,9 @@ python: - '3.6' install: - python setup.py install + - pip install pytest-cov script: - - python setup.py test \ No newline at end of file + - pytest --cov=pyas2lib +after_success: + - pip install codecov + - codecov \ No newline at end of file diff --git a/README.md b/README.md index e9060a6..38ce15b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # pyas2-lib +[![Build Status](https://travis-ci.org/abhishek-ram/pyas2-lib.svg?branch=master)](https://travis-ci.org/abhishek-ram/pyas2-lib) +[![codecov](https://codecov.io/gh/abhishek-ram/pyas2-lib/branch/master/graph/badge.svg)](https://codecov.io/gh/abhishek-ram/pyas2-lib) + A pure python library for building and parsing message as part of the AS2 messaging protocol. The message definitions follow the AS2 version 1.2 as defined in the [RFC 4130][1].The library is intended to decouple the message construction/deconstruction from the web server/client implementation. The following functionality is part of this library: * Compress, Sign and Encrypt the payload to be transmitted. diff --git a/setup.py b/setup.py index c11c93f..35516c5 100644 --- a/setup.py +++ b/setup.py @@ -5,12 +5,13 @@ install_requires = [ 'asn1crypto==0.24.0', 'oscrypto==0.19.1', - 'pyOpenSSL==17.5.0' + 'pyOpenSSL==17.5.0', ] tests_require = [ - 'nose', - 'requests', + 'pytest==3.4.0', + 'pytest-cov==2.5.1', + 'coverage==4.3.4' ] setup( From de5f508ad9820eac38ae995fc8d190f1157e50e7 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Thu, 15 Feb 2018 17:01:23 +0530 Subject: [PATCH 02/66] make attrs easily available --- pyas2lib/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index b56c16c..1e0ef9d 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -1,10 +1,23 @@ from __future__ import absolute_import import sys +from pyas2lib.as2 import DIGEST_ALGORITHMS, ENCRYPTION_ALGORITHMS,\ + MDN_CONFIRM_TEXT, MDN_FAILED_TEXT, Partner, Organization, Message, MDN -VERSION = (1, 0, '0b1') +VERSION = (1, 0, 0) __version__ = VERSION __versionstr__ = '.'.join(map(str, VERSION)) +__all__ = [ + 'VERSION', + 'DIGEST_ALGORITHMS', + 'ENCRYPTION_ALGORITHMS', + 'MDN_CONFIRM_TEXT', + 'MDN_FAILED_TEXT', + 'Partner', + 'Organization', + 'Message', + 'MDN' +] if (2, 7) <= sys.version_info < (3, 2): # On Python 2.7 and Python3 < 3.2, install no-op handler to silence From eb40e4ac553c0d40a3baf88f35185b8eaafd36f0 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Thu, 15 Feb 2018 17:06:10 +0530 Subject: [PATCH 03/66] bump version --- CHANGELOG.md | 6 ++---- README.md | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d25d9c6..0707343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,5 @@ # Release History -## 0.1.0 - dev +## 1.0.0 - 2018-02-15 -> **Note:** -> -> This version is not yet released and is under active development. +* Initial release. diff --git a/README.md b/README.md index 38ce15b..fd0e233 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # pyas2-lib +[![pypi package](https://img.shields.io/pypi/v/pyas2lib.svg)](https://pypi.python.org/pypi/pyas2lib/) [![Build Status](https://travis-ci.org/abhishek-ram/pyas2-lib.svg?branch=master)](https://travis-ci.org/abhishek-ram/pyas2-lib) [![codecov](https://codecov.io/gh/abhishek-ram/pyas2-lib/branch/master/graph/badge.svg)](https://codecov.io/gh/abhishek-ram/pyas2-lib) From 47461ce61b11c0489790969d86ea997aed563752 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 22 Apr 2018 20:06:44 +0530 Subject: [PATCH 04/66] Lots of changes --- .gitignore | 2 +- CHANGELOG.md | 9 ++ pyas2lib/__init__.py | 24 ++-- pyas2lib/as2.py | 174 ++++++++++++++--------- pyas2lib/utils.py | 51 ++++++- setup.py | 6 +- tests/__init__.py | 32 +++-- tests/fixtures/cert_oldpyas2_private.pem | 50 +++++++ tests/fixtures/cert_sb2bi_public.ca | 55 +++++++ tests/fixtures/cert_sb2bi_public.pem | 31 ++++ tests/fixtures/sb2bi_signed.mdn | Bin 0 -> 3052 bytes tests/fixtures/sb2bi_signed_cmp.msg | Bin 0 -> 4116 bytes tests/test_advanced.py | 78 +++++++--- tests/test_basic.py | 4 +- tests/test_mdn.py | 8 +- tests/test_with_mecas2.py | 8 +- 16 files changed, 412 insertions(+), 120 deletions(-) create mode 100644 tests/fixtures/cert_oldpyas2_private.pem create mode 100644 tests/fixtures/cert_sb2bi_public.ca create mode 100644 tests/fixtures/cert_sb2bi_public.pem create mode 100644 tests/fixtures/sb2bi_signed.mdn create mode 100644 tests/fixtures/sb2bi_signed_cmp.msg diff --git a/.gitignore b/.gitignore index e5b783d..fea5f0f 100644 --- a/.gitignore +++ b/.gitignore @@ -90,4 +90,4 @@ ENV/ # IDEA .idea -experiment.py \ No newline at end of file +.pytest_cache/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0707343..41276d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Release History +## 1.0.1 - 2018-04-22 + +* Check for incorrect passphrase when loading the private key. +* Change field name from `as2_id` to `as2_name` in org and partner +* Change name of class from `MDN` to `Mdn` +* Fix couple of validation issues when loading partner +* Return the traceback along with the exception when parsing messages +* Fix the mechanism for loading and validation partner certs + ## 1.0.0 - 2018-02-15 * Initial release. diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 1e0ef9d..7d801e5 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -1,22 +1,22 @@ from __future__ import absolute_import +# from pyas2lib.as2 import DIGEST_ALGORITHMS, ENCRYPTION_ALGORITHMS,\ +# MDN_CONFIRM_TEXT, MDN_FAILED_TEXT, Partner, Organization, Message, MDN import sys -from pyas2lib.as2 import DIGEST_ALGORITHMS, ENCRYPTION_ALGORITHMS,\ - MDN_CONFIRM_TEXT, MDN_FAILED_TEXT, Partner, Organization, Message, MDN VERSION = (1, 0, 0) -__version__ = VERSION -__versionstr__ = '.'.join(map(str, VERSION)) +__version__ = '.'.join(map(str, VERSION)) + __all__ = [ 'VERSION', - 'DIGEST_ALGORITHMS', - 'ENCRYPTION_ALGORITHMS', - 'MDN_CONFIRM_TEXT', - 'MDN_FAILED_TEXT', - 'Partner', - 'Organization', - 'Message', - 'MDN' + # 'DIGEST_ALGORITHMS', + # 'ENCRYPTION_ALGORITHMS', + # 'MDN_CONFIRM_TEXT', + # 'MDN_FAILED_TEXT', + # 'Partner', + # 'Organization', + # 'Message', + # 'MDN' ] if (2, 7) <= sys.version_info < (3, 2): diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index b85225b..5cf6f8d 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -3,8 +3,8 @@ from .cms import compress_message, decompress_message, decrypt_message, \ encrypt_message, verify_message, sign_message from .cms import DIGEST_ALGORITHMS, ENCRYPTION_ALGORITHMS -from .utils import canonicalize, mime_to_bytes, quote_as2name, unquote_as2name,\ - make_mime_boundary, extract_first_part, pem_to_der, \ +from .utils import canonicalize, mime_to_bytes, quote_as2name, unquote_as2name, \ + make_mime_boundary, extract_first_part, pem_to_der, split_pem, \ verify_certificate_chain from .exceptions import * from email import utils as email_utils @@ -15,6 +15,7 @@ import logging import hashlib import binascii +import traceback logger = logging.getLogger('pyas2lib') @@ -43,11 +44,11 @@ class Organization(object): """Class represents an AS2 organization and defines the certificates and settings to be used when sending and receiving messages. """ - def __init__(self, as2_id, sign_key=None, sign_key_pass=None, + def __init__(self, as2_name, sign_key=None, sign_key_pass=None, decrypt_key=None, decrypt_key_pass=None, mdn_url=None, mdn_confirm_text=MDN_CONFIRM_TEXT): """ - :param as2_id: The unique AS2 name for this organization + :param as2_name: The unique AS2 name for this organization :param sign_key: A byte string of the pkcs12 encoded key pair used for signing outbound messages and MDNs. @@ -68,7 +69,7 @@ def __init__(self, as2_id, sign_key=None, sign_key_pass=None, self.decrypt_key = self.load_key( decrypt_key, byte_cls(decrypt_key_pass)) if decrypt_key else None - self.as2_id = as2_id + self.as2_name = as2_name self.mdn_url = mdn_url self.mdn_confirm_text = mdn_confirm_text @@ -76,20 +77,31 @@ def __init__(self, as2_id, sign_key=None, sign_key_pass=None, def load_key(key_str, key_pass): """ Function to load password protected key file in p12 or pem format""" - # First try to parse as a p12 file try: + # First try to parse as a p12 file key, cert, _ = asymmetric.load_pkcs12(key_str, byte_cls(key_pass)) - except ValueError: + except ValueError as e: + # If it fails due to invalid password raise error here + if e.args[0] == 'Password provided is invalid': + raise AS2Exception('Password not valid for Private Key.') + + # if not try to parse as a pem file key, cert = None, None - # Now try to parse as a pem file - for kc in pem_to_der(key_str): + for kc in split_pem(key_str): try: - key = asymmetric.load_private_key(kc, key_pass) - except ValueError: cert = asymmetric.load_certificate(kc) - if not key or not cert: - raise AS2Exception( - 'Invalid Private key file or Public key not included.') + except (ValueError, TypeError): + try: + key = asymmetric.load_private_key(kc, + byte_cls(key_pass)) + except OSError: + raise AS2Exception( + 'Invalid Private Key or password is not correct.') + + if not key or not cert: + raise AS2Exception( + 'Invalid Private key file or Public key not included.') + return key, cert @@ -97,13 +109,13 @@ class Partner(object): """Class represents an AS2 partner and defines the certificates and settings to be used when sending and receiving messages.""" - def __init__(self, as2_id, verify_cert=None, verify_cert_ca=None, + def __init__(self, as2_name, verify_cert=None, verify_cert_ca=None, encrypt_cert=None, encrypt_cert_ca=None, validate_certs=True, compress=False, sign=False, digest_alg='sha256', encrypt=False, enc_alg='tripledes_192_cbc', mdn_mode=None, mdn_digest_alg=None, mdn_confirm_text=MDN_CONFIRM_TEXT): """ - :param as2_id: The unique AS2 name for this partner. + :param as2_name: The unique AS2 name for this partner. :param verify_cert: A byte string of the certificate to be used for verifying signatures of inbound messages and MDNs. @@ -148,12 +160,12 @@ def __init__(self, as2_id, verify_cert=None, verify_cert_ca=None, """ # Validations - if digest_alg not in DIGEST_ALGORITHMS: + if digest_alg and digest_alg not in DIGEST_ALGORITHMS: raise ImproperlyConfigured( 'Unsupported Digest Algorithm {}, must be ' 'one of {}'.format(digest_alg, DIGEST_ALGORITHMS)) - if enc_alg not in ENCRYPTION_ALGORITHMS: + if enc_alg and enc_alg not in ENCRYPTION_ALGORITHMS: raise ImproperlyConfigured( 'Unsupported Encryption Algorithm {}, must be ' 'one of {}'.format(enc_alg, ENCRYPTION_ALGORITHMS)) @@ -172,7 +184,7 @@ def __init__(self, as2_id, verify_cert=None, verify_cert_ca=None, 'Unsupported MDN Digest Algorithm {}, must be ' 'one of {}'.format(mdn_digest_alg, DIGEST_ALGORITHMS)) - self.as2_id = as2_id + self.as2_name = as2_name self.compress = compress self.sign = sign self.digest_alg = digest_alg @@ -181,22 +193,37 @@ def __init__(self, as2_id, verify_cert=None, verify_cert_ca=None, self.mdn_mode = mdn_mode self.mdn_digest_alg = mdn_digest_alg self.mdn_confirm_text = mdn_confirm_text - self.verify_cert = self.load_cert( - verify_cert, verify_cert_ca, validate_certs) if verify_cert else None - self.encrypt_cert = self.load_cert( - encrypt_cert, encrypt_cert_ca, validate_certs) if encrypt_cert else None + self.verify_cert = verify_cert + self.verify_cert_ca = verify_cert_ca + self.encrypt_cert = encrypt_cert + self.encrypt_cert_ca = encrypt_cert_ca + self.validate_certs = validate_certs + + def load_verify_cert(self): + if self.validate_certs: + # Convert the certificate to DER format + cert = pem_to_der(self.verify_cert, return_multiple=False) - @staticmethod - def load_cert(cert, cert_ca=None, validate_certs=True): - """ Function validates and loads the partners certificates""" - # Validate the certificate if option is set - if validate_certs: + # Convert the ca certificate to DER format + if self.verify_cert_ca: + trust_roots = pem_to_der(self.verify_cert_ca) + else: + trust_roots = [] + + # Verify the certificate against the trusted roots + verify_certificate_chain( + cert, trust_roots, ignore_self_signed=IGNORE_SELF_SIGNED_CERTS) + + return asymmetric.load_certificate(self.verify_cert) + + def load_encrypt_cert(self): + if self.validate_certs: # Convert the certificate to DER format - cert = pem_to_der(cert, return_multiple=False) + cert = pem_to_der(self.encrypt_cert, return_multiple=False) # Convert the ca certificate to DER format - if cert_ca: - trust_roots = pem_to_der(cert_ca) + if self.encrypt_cert_ca: + trust_roots = pem_to_der(self.encrypt_cert_ca) else: trust_roots = [] @@ -204,8 +231,7 @@ def load_cert(cert, cert_ca=None, validate_certs=True): verify_certificate_chain( cert, trust_roots, ignore_self_signed=IGNORE_SELF_SIGNED_CERTS) - # Return the parsed certificate - return asymmetric.load_certificate(cert) + return asymmetric.load_certificate(self.encrypt_cert) class Message(object): @@ -318,8 +344,8 @@ def build(self, data, filename=None, subject='AS2 Message', 'AS2-Version': AS2_VERSION, 'ediint-features': EDIINT_FEATURES, 'Message-ID': '<{}>'.format(self.message_id), - 'AS2-From': quote_as2name(self.sender.as2_id), - 'AS2-To': quote_as2name(self.receiver.as2_id), + 'AS2-From': quote_as2name(self.sender.as2_name), + 'AS2-To': quote_as2name(self.receiver.as2_name), 'Subject': subject, 'Date': email_utils.formatdate(localtime=True), # 'recipient-address': message.partner.target_url, @@ -387,10 +413,11 @@ def build(self, data, filename=None, subject='AS2 Message', encrypted_message.set_param('smime-type', 'enveloped-data') encrypted_message.add_header( 'Content-Disposition', 'attachment', filename='smime.p7m') + encrypt_cert = self.receiver.load_encrypt_cert() encrypted_message.set_payload(encrypt_message( canonicalize(self.payload), self.enc_alg, - self.receiver.encrypt_cert + encrypt_cert )) encoders.encode_base64(encrypted_message) self.payload = encrypted_message @@ -434,15 +461,16 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): as2-from header value is passed as an argument to it. :return: - A three element tuple containing (status, exception, mdn). The - status is a string indicating the status of the transaction. The - exception is populated with any exception raised during processing - and the mdn is an MDN object or None in case the partner did not - request it. + A three element tuple containing (status, (exception, traceback) + , mdn). The status is a string indicating the status of the + transaction. The exception is populated with any exception raised + during processing and the mdn is an MDN object or None in case + the partner did not request it. """ # Parse the raw MIME message and extract its content and headers - status, detailed_status, exception, mdn = 'processed', None, None, None + status, detailed_status, exception, mdn = \ + 'processed', None, (None, None), None self.payload = parse_mime(raw_content) as2_headers = {} for k, v in self.payload.items(): @@ -472,7 +500,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): ' but encrypted message not found.'.format(partner_id)) if self.payload.get_content_type() == 'application/pkcs7-mime' \ - and self.payload.get_param('smime-type') == 'enveloped-data': + and self.payload.get_param( + 'smime-type') == 'enveloped-data': self.encrypted = True self.enc_alg, decrypted_content = decrypt_message( self.payload.get_payload(decode=True), @@ -496,7 +525,7 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): self.signed = True signature = None message_boundary = ( - '--' + self.payload.get_boundary()).encode('utf-8') + '--' + self.payload.get_boundary()).encode('utf-8') for part in self.payload.walk(): if part.get_content_type() == "application/pkcs7-signature": signature = part.get_payload(decode=True) @@ -506,13 +535,16 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): # Verify the message, first using raw message and if it fails # then convert to canonical form and try again mic_content = canonicalize(self.payload) + verify_cert = self.sender.load_verify_cert() try: self.digest_alg = verify_message( - mic_content, signature, self.sender.verify_cert) + mic_content, signature, verify_cert) except IntegrityError: - mic_content = raw_content.split(message_boundary)[1] + print(self.payload.is_multipart()) + mic_content = raw_content.split(message_boundary)[ + 1].replace(b'\n', b'\r\n') self.digest_alg = verify_message( - mic_content, signature, self.sender.verify_cert) + mic_content, signature, verify_cert) # Calculate the MIC Hash of the message to be verified digest_func = hashlib.new(self.digest_alg) @@ -520,7 +552,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): self.mic = binascii.b2a_base64(digest_func.digest()).strip() if self.payload.get_content_type() == 'application/pkcs7-mime' \ - and self.payload.get_param('smime-type') == 'compressed-data': + and self.payload.get_param( + 'smime-type') == 'compressed-data': self.compressed = True decompressed_data = decompress_message( self.payload.get_payload(decode=True)) @@ -530,7 +563,7 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): status = getattr(e, 'disposition_type', 'processed/Error') detailed_status = getattr( e, 'disposition_modifier', 'unexpected-processing-error') - exception = e + exception = (e, traceback.format_exc()) finally: # Update the payload headers with the original headers for k, v in as2_headers.items(): @@ -547,8 +580,9 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): digest_alg = as2_headers.get('disposition-notification-options') if digest_alg: - digest_alg = digest_alg.split(';')[-1].split(',')[-1].strip() - mdn = MDN( + digest_alg = digest_alg.split(';')[-1].split(',')[ + -1].strip() + mdn = Mdn( mdn_mode=mdn_mode, mdn_url=mdn_url, digest_alg=digest_alg) mdn.build(message=self, status=status, @@ -557,13 +591,14 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): return status, exception, mdn -class MDN(object): +class Mdn(object): """Class for handling AS2 MDNs. Includes functions for both parsing and building them. """ def __init__(self, mdn_mode=None, digest_alg=None, mdn_url=None): self.message_id = None + self.orig_message_id = None self.payload = None self.mdn_mode = mdn_mode self.digest_alg = digest_alg @@ -606,13 +641,14 @@ def build(self, message, status, detailed_status=None): :param status: The status of processing of the received AS2 message. :param detailed_status: - The optional detailed status of processing of the received AS2 + The optional detailed status of processing of the received AS2 message. Used to give additional error info (default "None") - + """ # Generate message id using UUID 1 as it uses both hostname and time self.message_id = email_utils.make_msgid().lstrip('<').rstrip('>') + self.orig_message_id = message.message_id # Set up the message headers mdn_headers = { @@ -677,7 +713,7 @@ def build(self, message, status, detailed_status=None): message.receiver and message.receiver.sign_key: self.digest_alg = \ message.headers['disposition-notification-options'].split( - ';')[-1].split(',')[-1].strip() + ';')[-1].split(',')[-1].strip().replace('-', '') signed_mdn = MIMEMultipart( 'signed', protocol="application/pkcs7-signature") del signed_mdn['MIME-Version'] @@ -720,22 +756,22 @@ def parse(self, raw_content, find_message_cb): :param find_message_cb: A callback the must returns the original Message Object. The - original message-id and original recipient AS2 ID are passed + original message-id and original recipient AS2 ID are passed as arguments to it. :returns: A two element tuple containing (status, detailed_status). The status is a string indicating the status of the transaction. The - optional detailed_status gives additional information about the + optional detailed_status gives additional information about the processing status. """ status, detailed_status = None, None self.payload = parse_mime(raw_content) - orig_message_id, orig_recipient = self.detect_mdn() + self.orig_message_id, orig_recipient = self.detect_mdn() # Call the find message callback which should return a Message instance - orig_message = find_message_cb(orig_message_id, orig_recipient) + orig_message = find_message_cb(self.orig_message_id, orig_recipient) # Extract the headers and save it mdn_headers = {} @@ -754,7 +790,7 @@ def parse(self, raw_content, find_message_cb): if self.payload.get_content_type() == 'multipart/signed': signature = None message_boundary = ( - '--' + self.payload.get_boundary()).encode('utf-8') + '--' + self.payload.get_boundary()).encode('utf-8') for part in self.payload.walk(): if part.get_content_type() == 'application/pkcs7-signature': signature = part.get_payload(decode=True) @@ -764,17 +800,18 @@ def parse(self, raw_content, find_message_cb): # Verify the message, first using raw message and if it fails # then convert to canonical form and try again mic_content = extract_first_part(raw_content, message_boundary) + verify_cert = orig_message.receiver.load_verify_cert() try: self.digest_alg = verify_message( - mic_content, signature, orig_message.receiver.verify_cert) + mic_content, signature, verify_cert) except IntegrityError: mic_content = canonicalize(self.payload) self.digest_alg = verify_message( - mic_content, signature, orig_message.receiver.verify_cert) + mic_content, signature, verify_cert) for part in self.payload.walk(): if part.get_content_type() == 'message/disposition-notification': - mdn = part.get_payload().pop() + mdn = part.get_payload()[-1] mdn_status = mdn['Disposition'].split( ';').pop().strip().split(':') status = mdn_status[0] @@ -793,10 +830,10 @@ def parse(self, raw_content, find_message_cb): def detect_mdn(self): """ Function checks if the received raw message is an AS2 MDN or not. - - :raises MDNNotFound: If the received payload is not an MDN then this + + :raises MDNNotFound: If the received payload is not an MDN then this exception is raised. - + :return: A two element tuple containing (message_id, message_recipient). The message_id is the original AS2 message id and the message_recipient @@ -817,6 +854,7 @@ def detect_mdn(self): for part in mdn_message.walk(): if part.get_content_type() == 'message/disposition-notification': mdn = part.get_payload()[0] - message_id = mdn.get('Original-Message-ID') - message_recipient = mdn.get('Original-Recipient').split(';')[1] + message_id = mdn.get('Original-Message-ID').strip('<>') + message_recipient = mdn.get( + 'Original-Recipient').split(';')[1].strip() return message_id, message_recipient diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index 5655612..b4d3f91 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -10,10 +10,22 @@ def unquote_as2name(quoted_name): + """ + Function converts as2 name from quoted to unquoted format + + :param quoted_name: the as2 name in quoted format + :return: the as2 name in unquoted format + """ return email.utils.unquote(quoted_name) def quote_as2name(unquoted_name): + """ + Function converts as2 name from unquoted to quoted format + :param unquoted_name: the as2 name in unquoted format + :return: the as2 name in unquoted format + """ + if re.search(r'[\\" ]', unquoted_name, re.M): return '"' + email.utils.quote(unquoted_name) + '"' else: @@ -21,6 +33,12 @@ def quote_as2name(unquoted_name): def mime_to_bytes(msg, header_len): + """ + Function to convert and email Message to flat string format + :param msg: email.Message to be converted to string + :param header_len: the msx length of the header per line + :return: the byte string representation of the email message + """ fp = BytesIO() g = BytesGenerator(fp, maxheaderlen=header_len) g.flatten(msg) @@ -28,6 +46,12 @@ def mime_to_bytes(msg, header_len): def canonicalize(message): + """ + Function to convert an email Message to standard format string + + :param message: email.Message to be converted to standard string + :return: the standard representation of the email message in bytes + """ if message.is_multipart() \ or message.get('Content-Transfer-Encoding') != 'binary': @@ -95,6 +119,32 @@ def pem_to_der(cert, return_multiple=True): return cert_list.pop() +def split_pem(pem_bytes): + """ + Split a give PEM file with multiple certificates + :param pem_bytes: The pem data in bytes with multiple certs + :return: yields a list of certificates contained in the pem file + """ + started, pem_data = False, b'' + for line in pem_bytes.splitlines(False): + + if line == b'' and not started: + continue + + if line[0:5] in (b'-----', b'---- '): + if not started: + started = True + else: + pem_data = pem_data + line + b'\r\n' + yield pem_data + + started = False + pem_data = b'' + + if started: + pem_data = pem_data + line + b'\r\n' + + def verify_certificate_chain(cert_str, trusted_certs, ignore_self_signed=True): """ Verify a given certificate against a trust store""" @@ -123,4 +173,3 @@ 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]) - diff --git a/setup.py b/setup.py index 35516c5..ab52b17 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -from pyas2lib import __versionstr__ -from os.path import join, dirname + +version = __import__('pyas2lib').__version__ install_requires = [ 'asn1crypto==0.24.0', @@ -23,7 +23,7 @@ long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version=__versionstr__, + version=version, author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages( diff --git a/tests/__init__.py b/tests/__init__.py index 0551d00..666ce31 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -16,17 +16,33 @@ def setUpClass(cls): cls.test_data = t_file.read() with open(os.path.join( - cls.TEST_DIR, 'cert_test.p12'), 'rb') as key_file: - cls.private_key = key_file.read() + cls.TEST_DIR, 'cert_test.p12'), 'rb') as fp: + cls.private_key = fp.read() with open(os.path.join( - cls.TEST_DIR, 'cert_test_public.pem'), 'rb') as pub_file: - cls.public_key = pub_file.read() + cls.TEST_DIR, 'cert_test_public.pem'), 'rb') as fp: + cls.public_key = fp.read() with open(os.path.join( - cls.TEST_DIR, 'cert_mecas2_public.pem'), 'rb') as pub_file: - cls.mecas2_public_key = pub_file.read() + cls.TEST_DIR, 'cert_mecas2_public.pem'), 'rb') as fp: + cls.mecas2_public_key = fp.read() with open(os.path.join( - cls.TEST_DIR, 'cert_oldpyas2_public.pem'), 'rb') as pub_file: - cls.oldpyas2_public_key = pub_file.read() + cls.TEST_DIR, 'cert_oldpyas2_public.pem'), 'rb') as fp: + cls.oldpyas2_public_key = fp.read() + + with open(os.path.join( + cls.TEST_DIR, 'cert_oldpyas2_public.pem'), 'rb') as fp: + cls.oldpyas2_public_key = fp.read() + + with open(os.path.join( + cls.TEST_DIR, 'cert_oldpyas2_private.pem'), 'rb') as fp: + cls.oldpyas2_private_key = fp.read() + + with open(os.path.join( + cls.TEST_DIR, 'cert_sb2bi_public.pem'), 'rb') as fp: + cls.sb2bi_public_key = fp.read() + + with open(os.path.join( + cls.TEST_DIR, 'cert_sb2bi_public.ca'), 'rb') as fp: + cls.sb2bi_public_ca = fp.read() diff --git a/tests/fixtures/cert_oldpyas2_private.pem b/tests/fixtures/cert_oldpyas2_private.pem new file mode 100644 index 0000000..5360dee --- /dev/null +++ b/tests/fixtures/cert_oldpyas2_private.pem @@ -0,0 +1,50 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIBkTXi4mo7gUCAggA +MB0GCWCGSAFlAwQBKgQQegRDZRY+TL4QgRAWcqdz7wSCBNCGs2pr91erMuGIN8P0 +BQPmi4ohcUmhI+q/jtSYIGhkdsV9HTnVncMdr77RO+8xVn9QGpJFxTs+zhOzdsqK +SIcB1EbBjg2OXZ6tKID0Xc2L4ZBGLEV230m995IiiGqPkpF+JYqzcsbVTPFPgs/Y +V4crs3BAJfPw3jgH92++7C5Kvn7hLy8JYbnpGb8mEBoM4QIZ0forKPwOQTQtWQ/W +82hnEiMEoYIRjdWMAxPIdBBZEsMd55b0jVNxIW/1JZWM+yp9FZNrE7XobzVXUQSo +6CRqEYvBzJ6+X223bMac8X100zl7NzXxCns8/JN9ts+6x89cqdhCxwjLRlixovvR +tPla9JFFLvie94gd685D5WtKfg6srhNCqr2j8nSIC3FrXyAnOeLJavznxOstey5b +jt1zK6RunZgO9SuBpq5qSr4huUiH7edbMw8WnNhY/ur51GBpjiE4XIVbsPVoeCjz +CL4XgGoJExOSj7n3IvZCTXKxQ9G9M8nk3LYPmIuAEKFduErarwML3GtJsj1r6HbD +BS+WD18BZv4YhhLqPIlwwPszVUNp6NX/AGhs8URGYTTV5iA37doKR+rN6GXVCwDA +Us4xdIdNfVMs/PNDlV4qZ4VrnC2w/6n3N2/K1EwmDPxICaWAtnXBDWMQiXjXPsSW +8TUIjZMCFgzIqg7PSh4wtIlaz4xmV3EIjyw2oQVc8NRK01TEhW4ZvEi3WEQB16Un +fu24OpNPSDYyeUBbnNQ1bF+Hg2d2d/OiGR8Fma/dW2obhFJVz7CDXt0LeazYR3wZ +4tIdec4t5cxRb+CTlpdv+4AcvPYC3y4nwDYxUAO1rKQthnFCkrWm/Uy6PxbF83NS +95/6YuMoKb3bYZQSe566F5t/SL6zEYOFNiLWRbYqtLE1ICoAKus7AhbaLQuLsVW0 +/0zYCDVuc5cNt3Lk1MlFc7m8h+jVRroQpipdG+5UslSNYZJIh5QhGPcly+c0vHpa +mDIX3nv7o4SdJAOuEek4XLRb9Ov/2JC3ajkr2NLXiNyH2HUvDGVujLzOE2ZfUozE +u8EjmYY1bTIJuq5+86IRt5QWchs1jWKklgo3PIpZGmUk2CxdgyoA9BHFR9TVQn+j +BxeKDJFbYX5GG/pByFe2EMllF6yN4PQyeEUMFlo0Tv5o0vsh2nwlkUZxpvPfU188 +M+YIZ9wv1Wkcv+xjjHCSMuLDFS5Ajc+5as7eE0gwKabmEmj/z8/0b/cyLBfbpOt7 +87lul9b5Se5UJd83rRZE+QjFUzRWxeGJcHrFh6s3eTJIJUlQLhlcbUSowq7hWjti +0j+YIkGmFdk5QpEXR/+3DYUwXAemoBWz3eXs4NF09mZ/qASpA67xU7tXKMxl+Aoh +HHS89RiTUetdu4Y6GiLtp31D5VsEJvqel0beiS16KK1mayPJjW1FOLcH2X56Ew04 +d+2dsFEk/3gGTrHP8MXJqa3kHjc0erdVlIkJBpB91A5VQjUaYXA6V+kMeKKUAZFg +2bolAGt1i3aCpYtosGEOdrlP7LTcwBi6QgmJs6IBuELnT5F+kyGEdqgwyhToduHS +yQ9e/pyExp8aK3YOqMCSP9yj49aPHkhmnyKNp6pjw1ar3EY3sUcWvhyDhq41sjq6 +hPQEpLerIScoJyTuDTg0WfIlIA== +-----END ENCRYPTED PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDLjCCAhYCCQD7SuhYur9VTTANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJJ +TjETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRIwEAYDVQQDDAlhczJzZXJ2ZXIwHhcNMTcxMjIwMDkxNDU3WhcN +MjIxMjE5MDkxNDU3WjBZMQswCQYDVQQGEwJJTjETMBEGA1UECAwKU29tZS1TdGF0 +ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlh +czJzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNC0Vq/TJz +Qasi8VqCvMhDRSN68C00J4GbDGxbbIjpXZ9+4wKd7JcPm6HOsVfQK0xxbtox/BGP +392Hwz2gjn9nEgwuE7wJD0c5zFrRcQlG1avF0CBgFPa1PuSyTrH8JJ+OFK5dO0Su +W5RfaW/CAwp65mVYcGhWKvQ+D7alCvQ8VD50PMj/vw3Wdsry9TQ44VtAz5WUK/pd +AeNbnyMRS4984o4ycUiGWI1hI3xgzFGmLrBEaSBMxTOrifpx1SnE8pRkX3iLO93E +ec0QlNdUdc+mSOSEnEayANuxcQFegFIDajMs+jGVzCu0hDfQx9IfKSwhIc/Ay0v9 +ONyTdapqopgVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAESihgqCRSt5v2aIcoUj +BgvX4nZw0RP/rD8cGc3LA/kDLiRne/tZRzCNPPZrEjO3pyBwI1XzaFP6jxcFPVPS +JefBg2KZVoY2sRNaoXru9x8QWVbrdwWaoLg+v6Oj9iwr9CqMMEa9xegEjje87Sga +gSfYasgEWmx0g+l1rbLxkBeUfDKSGRMqi3r9MryV9udTao+4G7H8WFSHThActlxV +BQAHYVMlr6+O16AMwbKwJWIJ5jT8F8pe4aWZWtd0+EzrmP0MxQvEgObD1gk28miQ +6MkIbx+9vkBytM9LlME1YUWvQfBy8lKt93+FiTDplpwgDau7pTGGqXJen/wYjq+w +Bxo= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/cert_sb2bi_public.ca b/tests/fixtures/cert_sb2bi_public.ca new file mode 100644 index 0000000..9a259ac --- /dev/null +++ b/tests/fixtures/cert_sb2bi_public.ca @@ -0,0 +1,55 @@ +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFDjCCA/agAwIBAgIMDulMwwAAAABR03eFMA0GCSqGSIb3DQEBCwUAMIG+MQsw +CQYDVQQGEwJVUzEWMBQGA1UEChMNRW50cnVzdCwgSW5jLjEoMCYGA1UECxMfU2Vl +IHd3dy5lbnRydXN0Lm5ldC9sZWdhbC10ZXJtczE5MDcGA1UECxMwKGMpIDIwMDkg +RW50cnVzdCwgSW5jLiAtIGZvciBhdXRob3JpemVkIHVzZSBvbmx5MTIwMAYDVQQD +EylFbnRydXN0IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjAeFw0x +NTEwMDUxOTEzNTZaFw0zMDEyMDUxOTQzNTZaMIG6MQswCQYDVQQGEwJVUzEWMBQG +A1UEChMNRW50cnVzdCwgSW5jLjEoMCYGA1UECxMfU2VlIHd3dy5lbnRydXN0Lm5l +dC9sZWdhbC10ZXJtczE5MDcGA1UECxMwKGMpIDIwMTIgRW50cnVzdCwgSW5jLiAt +IGZvciBhdXRob3JpemVkIHVzZSBvbmx5MS4wLAYDVQQDEyVFbnRydXN0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5IC0gTDFLMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA2j+W0E25L0Tn2zlem1DuXKVh2kFnUwmqAJqOV38pa9vH4SEkqjrQ +jUcj0u1yFvCRIdJdt7hLqIOPt5EyaM/OJZMssn2XyP7BtBe6CZ4DkJN7fEmDImiK +m95HwzGYei59QAvS7z7Tsoyqj0ip/wDoKVgG97aTWpRzJiatWA7lQrjV6nN5ZGhT +JbiEz5R6rgZFDKNrTdDGvuoYpDbwkrK6HIiPOlJ/915tgxyd8B/lw9bdpXiSPbBt +LOrJz5RBGXFEaLpHPATpXbo+8DX3Fbae8i4VHj9HyMg4p3NFXU2wO7GOFyk36t0F +ASK7lDYqjVs1/lMZLwhGwSqzGmIdTivZGwIDAQABo4IBDDCCAQgwDgYDVR0PAQH/ +BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwMwYIKwYBBQUHAQEEJzAlMCMGCCsG +AQUFBzABhhdodHRwOi8vb2NzcC5lbnRydXN0Lm5ldDAwBgNVHR8EKTAnMCWgI6Ah +hh9odHRwOi8vY3JsLmVudHJ1c3QubmV0L2cyY2EuY3JsMDsGA1UdIAQ0MDIwMAYE +VR0gADAoMCYGCCsGAQUFBwIBFhpodHRwOi8vd3d3LmVudHJ1c3QubmV0L3JwYTAd +BgNVHQ4EFgQUgqJwdN28Uz/Pe9T3zX+nYMYKTL8wHwYDVR0jBBgwFoAUanImetAe +733nO2lR1GyNn5ASZqswDQYJKoZIhvcNAQELBQADggEBADnVjpiDYcgsY9NwHRkw +y/YJrMxp1cncN0HyMg/vdMNY9ngnCTQIlZIv19+4o/0OgemknNM/TWgrFTEKFcxS +BJPok1DD2bHi4Wi3Ogl08TRYCj93mEC45mj/XeTIRsXsgdfJghhcg85x2Ly/rJkC +k9uUmITSnKa1/ly78EqvIazCP0kkZ9Yujs+szGQVGHLlbHfTUqi53Y2sAEo1GdRv +c6N172tkw+CNgxKhiucOhk3YtCAbvmqljEtoZuMrx1gL+1YQ1JH7HdMxWBCMRON1 +exCdtTix9qrKgWRs6PLigVWXUX/hwidQosk8WwBD9lu51aX8/wdQQGcHsFXwt35u +Lcw= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/cert_sb2bi_public.pem b/tests/fixtures/cert_sb2bi_public.pem new file mode 100644 index 0000000..2c2de76 --- /dev/null +++ b/tests/fixtures/cert_sb2bi_public.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFQzCCBCugAwIBAgIRAPyxGMfr4QFOAAAAAFDb+P8wDQYJKoZIhvcNAQELBQAw +gboxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL +Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg +MjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLjAs +BgNVBAMTJUVudHJ1c3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBMMUswHhcN +MTcwMzI0MTgwODAzWhcNMjAwNjIzMTgzODAxWjBoMQswCQYDVQQGEwJVUzEOMAwG +A1UECBMFVGV4YXMxDzANBgNVBAcTBlRlbXBsZTEdMBsGA1UEChMUTWNMYW5lIENv +bXBhbnksIEluYy4xGTAXBgNVBAMTEGIyYi5tY2xhbmVjby5jb20wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDDDv75fHPPyLdBRQkNlkQuk2sKO//abQ+L +mp2xSrHWnnWpTAcuu9H6MQDKPkQKu0mUzNh1WexfmrnutuZeLOmJLTujTuYqp9Hc +p6jKlsQeR9tBe1lVFRNI7L1uHdwiiBFOhViUwsBwWDO1fZ7aUfBXQJrduxeE6O42 +8PaPtk9LP5vsFj6L8MKDskbNee8/v24MPje5e+EesLxmVRESSnC8xV651BQT6lzU +m/bFEzGV8RQLo1Cv6/Qsdb56I2PZYWJyLVItB4h3WvCuZYkwZtzZCShaW4ZV1hGS +IBlHyrvX7CARBTeBWsx0myu7Emvshrk1mSfRwxsm8jTMK/RVP7VVAgMBAAGjggGT +MIIBjzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwMwYDVR0f +BCwwKjAooCagJIYiaHR0cDovL2NybC5lbnRydXN0Lm5ldC9sZXZlbDFrLmNybDBL +BgNVHSAERDBCMDYGCmCGSAGG+mwKAQUwKDAmBggrBgEFBQcCARYaaHR0cDovL3d3 +dy5lbnRydXN0Lm5ldC9ycGEwCAYGZ4EMAQICMGgGCCsGAQUFBwEBBFwwWjAjBggr +BgEFBQcwAYYXaHR0cDovL29jc3AuZW50cnVzdC5uZXQwMwYIKwYBBQUHMAKGJ2h0 +dHA6Ly9haWEuZW50cnVzdC5uZXQvbDFrLWNoYWluMjU2LmNlcjAxBgNVHREEKjAo +ghBiMmIubWNsYW5lY28uY29tghR3d3cuYjJiLm1jbGFuZWNvLmNvbTAfBgNVHSME +GDAWgBSConB03bxTP8971PfNf6dgxgpMvzAdBgNVHQ4EFgQUiTGHatRioLvWttlv +dXrAH6AKVbIwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAAr7qLcJUTyYc +9COEI84PTLZx1tRbzZWBzVk3Xa3ucNJY+ifii9LmiU9IZS+Mn+ikN71u7Npj3PBq +rutMS03P/ILObp1Ankil2jVxpQcy1d/VGKSis9IhMmQWzIUqOFfsbZ6SsirQA2Y4 +Vz5GGyR6VG37Gn5fZJXKXeottFVVLZkE+/FC+AJJWNGEgt3quo6GENz+3/LgtkUZ +KGlIIBNCrB6/mTTR03MKYiNtxrMgnmcgVtc8W8g1zsXdaTKcpJzZwVKwNuqw6YfE +teP7dKGOKPvqL/i23fHBBKx7DdHRlvVlL04FAK/YuXq06TFOf/AG626j3uxwmRQ2 +7AL3IEu8sA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/sb2bi_signed.mdn b/tests/fixtures/sb2bi_signed.mdn new file mode 100644 index 0000000000000000000000000000000000000000..456dbd3ddf1d5613f76ad852e4d9f6dbbe98e426 GIT binary patch literal 3052 zcmdT`dsGu=7SDtv=s*-2#J7&1h{`12OhOV!K@kurFBiy)`06kjU}`cmotcz^wqjJ& zqpjAeyP#!JY27_qMXR_Jt@XJckEp1{s<_(j(XEJz9ILGM=+5w{2-}|B{Dwyf+TQj1j9NgV`^+lOj>+wdQ2uh zJtJ;PE}qNr4q()~NGHKiB&#P`S2Rqrj7TveDi(26B;s;AMVcdc(IC+I4CSzKJS(y! z>x>E_IL=9vgh;cDftybX7F0GNL^n?b*<6ywnI9z-5ST5Obu$ivFNzA97iBbBG0civ zjTSs29K$eErbzKln#u2@kTVH_VHEveL0UMrD}}6HDex4>@}e!H;}4a!Q4U(*Sb>(4 zKp9q~^SUy6C1|tN3L>l~i^*gLR$vP6j=8tIBV;YC?FhQJ^4CBzW+vAchMOQn$SNayK%njxHM21U{w zE%74~;qyokl@^}%U)ufbGPU!5G$~HvFBXoOBTN8TA~35SH<=@V-U7mL++xIm z)f8@x01>z;dm<30T7>b!t97O0T64%obad|1oXe}&-*-t-xX? z;%PE(s+-R@P$FJPv!>$t20SS$Os1+9j(f?6NF>HBvM!0>BubDSG*=++QY zbL5%m?8k3u2a0y1`iwv~W%?*&qV=YE%rB3ljUilARD4`gQd(AKQi7hd2=bl4<6Wrf zKe05i^obVoR#tkv3nY<42|<_-=&w;v8sMcIrd9x-mqt*fN1XsX>R8}WO{(-#LQ18- z;-@`B{&x2}C`}>#reAA+0kl5@ARh_8_{$D|UUrp6r?h8cfgnI59bla{o)LMsAci4H z45`OLL5M8((T&ceD5S8kP%o8Q*6SHcG&reziGNauT>=&XES;7hl$?Oz0K<`2Tp=iu z$MT5Or2>|xi>0w~3lzdK<_?!epqIZd?}viKY0Lb1(N?#)w6dj}$B!j2f)PEWn7H zFsn5C#+<=g8~`(J#H>auz;ZyrtGNvTzOws%I&~KHE+Jq8rD8~DZ=EKKa&b-y8wrNX z0$3N2N~TC_7l~zE9KjTI)nF(X+!>yqw4Ptt%)kQ$^{gzUR(N@!feJ|uD?JdTI5yz< zzZMGTP92De@2}k$r~i09Yn=Cwt!5(^%pDONzI?uHh-BI2|1a*Cnk#^R0Vk z%e?a1`!%=chTU0-+P0N89!x&W_(of57koFW z`e2^j|Ft)`gD2+JUJB4P&%U(j@d+Ka;bDN!w&}a?J_>VxRUAxSA#!;%1NB}}nA5t8 zS_$%+uJjMhnN@0U^k0VzO*mb5xdri8TNdY>5jRb$du@J8X>EAf_=aP{L)uJdCOxuG z-fvfWK?-P_2l`m5$Qm#}D&xq35cEQ&@=~fRflfY;Q~3f|({GXnQmehaAYhb*qg7#G zA_%PvsT^AxR3M66q`^S)&R*nuhjL=`^^zPU%OFG*2c`nE27a?N5h{J^gdsJM`P=1N z2?Y-8a@Cv1JV$_j8cqIUUuk$iK{o<|RI@=22<|=usC00b2bL7Lp3z8QcU=RNrQ^Gd z2%6|=W00an$pV6A@Nl!9qw0q<+!%H&4PThrvkMV1O!66|fRp{??`3%DTpyE3CWt>(Q04eXq1bB(+YZ1d5)- zl~#g8Tu7n(su?|+l^!zUQSj2>vjbCV-fp}!tA4}c`k9tFpWo*$WIrAM{i+MMSEeUY zhSh)ip~`ZIX}L-^wZ66MZc1|Mxt}~|nNOxvBz}4|{OwP@@o#Q?Go)(k7Z=9hj=(d^ zCR%5-xGL7|op|0W&pIP|+VHW(S*~Zp-k;~#aC%NNy3cM$%T&)EPHk5vWj8GKTyOsJ z!&1Md=QrAJ*2E7Dr4td|)SaUamzf$a3UF?)>*Nas%m0{Ok)+J8<^3fmj6U92*eH6Oore^BM)jB6hph-!n={_x(rou7z( N-t}L7@Z}GMe*P#EV6cRIe->s#EMp9&=F5Ee2w|#`<&;&dN}5!g zR4b{mA}U!@Rtih&BW>Hqu&7npRh)Oqp4vL+T-Q0*b^bW>&vQNZ^SgiV{kxxg?&qd) z*aA9RKw#3@34%likOY%ptdLLV5vU1>-2wFS39;J1_ZA4a>zNG1;u=Tg3g|p6jl*Ws zX#xg^?Euo496lY(7shTuEDoR#LYcs$3OGD4fX?S5L|7`HNDy!wKx9R@lM#Qp1zGuT z31CYiHkA<1;iXV{ar8I>m&cK#6OpfC6FGc@Q^2FL`Aj*drI3yI&=7vQC5^*!AVb(V z2A|8}GvvV#*c<^PoxK-Lb59dI^j;2N1HcBa!T_VO!Y7 z*52OEIs{1@lfh0PetZxRHOLe35B@krwFBtMV_1MhThN!v28j?%24QOl64`-h z19}C7%5UcFl?u|h2$m&e3K(1}Phj;5@6s6E@%$%c7+|m zSdNe#N9CnC8ALmMBH-UDGr(dAg#Xm(BN3kl%3`qSjv!yY5adFgkW%3CknAJ3j3kNb z2(qaxy3<>LCD)FH#X{Lii_OK};#aDGiUyz}$%e8~fC8lYfmLx40#aE3DFc#hqii%P zwG1snH=O;P#Xsb!F;{-zb?%$e!S?;@8_ihOt=Y`h2Pd*)9ar76zj~v|47IEfmpNLO zqHb0syL2Gg#7;*k`BnMjfy)g}^}^m~RSi#)b?4N|I={i&kGV&^xu0H}?^tztd+{na zY&To>x%Xqon5eerZ8rC^b{4NmYwdge7_tzctIKSZd%oFP_knEgkkUwQoz;1CaE8MHn&GK!_EFAhO^(zh2J;%aJG22T| z6wWkT#^+7uoVY41(%%uknmp`fD#*k{n)<|VM}?#Fx%&3QPnMT>`q-jOr@*pCYK9W%e9`v zhc{Pll7w(is+7twlxf2W-IvLNM>?mgVkN2=?Sy#0lcu%!)hi;@jAsiXbhhurQNFM% zT|Jfx>}?#aDB7mebnTV-V{BZ@b@4jp%~8R{a}1eR4PNhlO^0rcR_}eX-{HKfUtRiw z&Y)5GRT@DRS0=Se&UpAm`FK?1NNGdTVq>@dJ&%LiaFeMmg{Rm*>?{YJ^CvbI(SG+` zecD;)`D^W{DT?~qo7$u0W?RDc?-a$To2Pj0D3@)J7*3@f zRa<*0I=gZEwIbi**iP4EEhCxi8@&s4KmAgFik4Ld4s0<#XcOz4a_Zyq|#eo znu@pXqC1EEqAz;9>~!5wy>B0W>7&g9&VIt_E@KCa5l!tee&pAOQRi)=gOo~|&yHs4 zC`cv^n2CD}}v^ILMe z4(_aeoV`&9B?WalZ)L9H-iHrHH5qlqaS=%wu@qnF^^9M>3jU*T>3G@fRt2|tE$0~@ z*~q@uNU3XQpQ)I&v3H^ct`VpXCg#lHq2WWjHWj93R^rCvk7gaYb{p+8<7&&~-!Yxr z|3v2{ozL75V(ssvolacoXdF39|+ z&&%N+=kl~T^C(7P)OwBjAJvZMFW0@O8*p+_m7l-;R8y&8Ing%HacFZ>C!?wTl#Xej zoyx>0Z(=yZ%6H7em&L1CvhvKz$gG3811Sc*J=#7mp5@`IUA~)s_H1OukcFYcaQ%#K zV^1*Y;o+Bog__d=OO{O@>@mGmKfjp!3(oh#_D9FA-;FMqvr_K7@lxS~-~XZ&zkf8} z^;{2kAD_U`5Fan3@A8qpD^gB*-&9c&k#ZtpibDYayckkZ!mL$OMCo7@pvCV&fJB7x zf<&10kO;k2qKE=eC{2aeHOt$^Mu9-NnhihA%p;xq&sGgAM*MRwz2#G+pp|eaN(ih4 z;gygL7N_dL7Vw08fdw*{&@ACKkcr&B7`G~fP6ty`QY?|FMt*L|rVFf?^aLuCfV|W! zK5P%!y+cB4XlC+lDd9Z3eVSH>xY(pg+49oB<%U!alOp})9w~sljo#cYo{seE>cj6fC6w#1nfgHvKLZA(x|5n0P|?H zA_^mcaPqw|dI^M8TC`RPz+jXW0mxczT!prP%%L?B6N!=7Kt9+ySXt3{%#VsMna+f_ zS|WDHPwoVw-61!~RtXy;_6Ed%FtGpzlGpD7Hww_wS)ls3j(J=vv`9%QA!iBl=YbOc z^Z)>KBoqM|{<(($vGxK3hsNiA7!4Bk!e=0q*mMCzWl%rFSRv68Xo*w?n`mQeNu%>1 zSRS?}8c{0xq_#wOd6b`2+$y)#CN$pLC; zEi^t4&fU@zD{1JH4se9quB?(^Dd&+Mf*j+}C`dlceb^F^!UYsiO)|o@&>)kQ(}p_@ ze^B?ACiV4f?kvvf47ZCqJHfpZ_J`?%{5y~Gg1qTg1xKD7x4X<9zegLK+Vbs~zh6Mt zYtawv!!G6CC+^uKolqwBj`S`+UQv5zH8D=>)-H4Mrg2tz(RuURit*%4&YrqP+d^5h zI@_b;iaVlYgbNf3;Q)H}ncED?C#-v?Xh?Qxk62@HZshrWsmF>n3~vzUc1FMX0IB;f z9~*1P`o0z{PXIsv@w3hCHb1luF^FZy%Lc9npR<*ndzyRw;?J{!qkGoO%B*IjL(i_F z&!nq%cbB}NTLoeis{8A=U3dxyW=$!Lv5yUpa}VHc$5F39zs7U02$(@ibsFinP|ofD z>n{1v9)S-YkPj{&=~`$JppbFp#G8!OO@Zr-mZ#&1Z(N@9D=s+ID&C&GZF~1dTGd=_ z?%s^;S^+XKqAoHtrtFe4Wvn+a literal 0 HcmV?d00001 diff --git a/tests/test_advanced.py b/tests/test_advanced.py index 3c8df7d..8f01f40 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -7,14 +7,14 @@ class TestAdvanced(PYAS2TestCase): def setUp(self): self.org = as2.Organization( - as2_id='some_organization', + as2_name='some_organization', sign_key=self.private_key, sign_key_pass='test'.encode('utf-8'), decrypt_key=self.private_key, decrypt_key_pass='test'.encode('utf-8') ) self.partner = as2.Partner( - as2_id='some_partner', + as2_name='some_partner', verify_cert=self.public_key, encrypt_cert=self.public_key, ) @@ -72,7 +72,7 @@ def test_partner_not_found(self): find_partner_cb=self.find_none ) - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( mdn.headers_str + b'\r\n' + mdn.content, find_message_cb=self.find_message @@ -88,7 +88,7 @@ def test_partner_not_found(self): find_partner_cb=self.find_partner ) - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( mdn.headers_str + b'\r\n' + mdn.content, find_message_cb=self.find_message @@ -116,7 +116,7 @@ def test_insufficient_security(self): find_partner_cb=self.find_partner ) - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( mdn.headers_str + b'\r\n' + mdn.content, find_message_cb=self.find_message @@ -129,8 +129,8 @@ def test_failed_decryption(self): # Build an As2 message to be transmitted to partner self.partner.encrypt = True - self.partner.encrypt_cert = self.partner.load_cert( - self.mecas2_public_key, validate_certs=False) + self.partner.encrypt_cert = self.mecas2_public_key + self.partner.validate_certs = False self.partner.mdn_mode = as2.SYNCHRONOUS_MDN self.out_message = as2.Message(self.org, self.partner) self.out_message.build(self.test_data) @@ -145,7 +145,7 @@ def test_failed_decryption(self): find_partner_cb=self.find_partner ) - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( mdn.headers_str + b'\r\n' + mdn.content, find_message_cb=self.find_message @@ -158,8 +158,8 @@ def test_failed_signature(self): # Build an As2 message to be transmitted to partner self.partner.sign = True - self.partner.verify_cert = self.partner.load_cert( - self.mecas2_public_key, validate_certs=False) + self.partner.verify_cert = self.mecas2_public_key + self.partner.validate_certs = False self.partner.mdn_mode = as2.SYNCHRONOUS_MDN self.out_message = as2.Message(self.org, self.partner) self.out_message.build(self.test_data) @@ -174,7 +174,7 @@ def test_failed_signature(self): find_partner_cb=self.find_partner ) - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( mdn.headers_str + b'\r\n' + mdn.content, find_message_cb=self.find_message @@ -190,7 +190,7 @@ def test_verify_certificate(self): with open(cert_path, 'rb') as cert_file: try: as2.Partner( - as2_id='some_partner', + as2_name='some_partner', verify_cert=cert_file.read() ) except as2.AS2Exception as e: @@ -202,7 +202,7 @@ def test_verify_certificate(self): with open(cert_path, 'rb') as cert_file: try: as2.Partner( - as2_id='some_partner', + as2_name='some_partner', verify_cert=cert_file.read() ) except as2.AS2Exception as e: @@ -214,7 +214,7 @@ def test_verify_certificate(self): with open(cert_path, 'rb') as cert_file: try: as2.Partner( - as2_id='some_partner', + as2_name='some_partner', verify_cert=cert_file.read() ) except as2.AS2Exception as e: @@ -227,7 +227,7 @@ def test_verify_certificate(self): with open(cert_ca_path, 'rb') as cert_ca_file: try: as2.Partner( - as2_id='some_partner', + as2_name='some_partner', verify_cert=cert_file.read(), verify_cert_ca=cert_ca_file.read() ) @@ -242,7 +242,7 @@ def test_load_private_key(self): with open(cert_path, 'rb') as cert_file: try: as2.Organization( - as2_id='some_org', + as2_name='some_org', sign_key=cert_file.read(), sign_key_pass=b'test' ) @@ -254,7 +254,7 @@ def test_load_private_key(self): with open(cert_path, 'rb') as cert_file: try: as2.Organization( - as2_id='some_org', + as2_name='some_org', sign_key=cert_file.read(), sign_key_pass=b'test' ) @@ -272,3 +272,47 @@ def find_none(self, as2_id): def find_message(self, message_id, message_recipient): return self.out_message + + +class SterlingIntegratorTest(PYAS2TestCase): + + def setUp(self): + self.org = as2.Organization( + as2_name='AS2 Server', + sign_key=self.oldpyas2_private_key, + sign_key_pass='password'.encode('utf-8'), + decrypt_key=self.oldpyas2_private_key, + decrypt_key_pass='password'.encode('utf-8') + ) + self.partner = as2.Partner( + as2_name='Sterling B2B Integrator', + verify_cert=self.sb2bi_public_key, + verify_cert_ca=self.sb2bi_public_ca, + encrypt_cert=self.sb2bi_public_key, + encrypt_cert_ca=self.sb2bi_public_ca, + ) + + def xtest_process_message(self): + """ Test processing message received from Sterling Integrator""" + with open(os.path.join(self.TEST_DIR, 'sb2bi_signed_cmp.msg')) as msg: + as2message = as2.Message() + status, exception, as2mdn = as2message.parse( + msg.read(), + lambda x: self.org, + lambda y: self.partner + ) + print(status, exception, as2mdn) + self.assertEqual(status, 'processed') + + def test_process_mdn(self): + """ Test processing mdn received from Sterling Integrator""" + message = as2.Message(sender=self.org, receiver=self.partner) + message.message_id = '151694007918.24690.7052273208458909245@' \ + 'ip-172-31-14-209.ec2.internal' + + as2mdn = as2.Mdn() + # Parse the mdn and get the message status + with open(os.path.join(self.TEST_DIR, 'sb2bi_signed.mdn')) as mdn: + status, detailed_status = as2mdn.parse( + mdn.read(), lambda x, y: message) + self.assertEqual(status, 'processed') diff --git a/tests/test_basic.py b/tests/test_basic.py index b8dbba1..9486528 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -6,14 +6,14 @@ class TestBasic(PYAS2TestCase): def setUp(self): self.org = as2.Organization( - as2_id='some_organization', + as2_name='some_organization', sign_key=self.private_key, sign_key_pass='test'.encode('utf-8'), decrypt_key=self.private_key, decrypt_key_pass='test'.encode('utf-8') ) self.partner = as2.Partner( - as2_id='some_partner', + as2_name='some_partner', verify_cert=self.public_key, encrypt_cert=self.public_key ) diff --git a/tests/test_mdn.py b/tests/test_mdn.py index 5125069..70002c7 100644 --- a/tests/test_mdn.py +++ b/tests/test_mdn.py @@ -7,14 +7,14 @@ class TestMDN(PYAS2TestCase): def setUp(self): self.org = as2.Organization( - as2_id='some_organization', + as2_name='some_organization', sign_key=self.private_key, sign_key_pass='test'.encode('utf-8'), decrypt_key=self.private_key, decrypt_key_pass='test'.encode('utf-8') ) self.partner = as2.Partner( - as2_id='some_partner', + as2_name='some_partner', verify_cert=self.public_key, encrypt_cert=self.public_key, ) @@ -40,7 +40,7 @@ def test_unsigned_mdn(self): find_partner_cb=self.find_partner ) - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( mdn.headers_str + b'\r\n' + mdn.content, find_message_cb=self.find_message @@ -69,7 +69,7 @@ def test_signed_mdn(self): find_partner_cb=self.find_partner ) - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( mdn.headers_str + b'\r\n' + mdn.content, find_message_cb=self.find_message diff --git a/tests/test_with_mecas2.py b/tests/test_with_mecas2.py index 3b0ed68..75d5e86 100644 --- a/tests/test_with_mecas2.py +++ b/tests/test_with_mecas2.py @@ -7,14 +7,14 @@ class TestMecAS2(PYAS2TestCase): def setUp(self): self.org = as2.Organization( - as2_id='some_organization', + as2_name='some_organization', sign_key=self.private_key, sign_key_pass='test'.encode('utf-8'), decrypt_key=self.private_key, decrypt_key_pass='test'.encode('utf-8') ) self.partner = as2.Partner( - as2_id='mecas2', + as2_name='mecas2', verify_cert=self.mecas2_public_key, encrypt_cert=self.mecas2_public_key, validate_certs=False @@ -120,7 +120,7 @@ def test_unsigned_mdn(self): # Parse the generated AS2 message as the partner received_file = os.path.join(self.TEST_DIR, 'mecas2_unsigned.mdn') with open(received_file, 'rb') as fp: - in_message = as2.MDN() + in_message = as2.Mdn() status, detailed_status = in_message.parse( fp.read(), find_message_cb=self.find_message) @@ -133,7 +133,7 @@ def test_signed_mdn(self): # Parse the generated AS2 message as the partner received_file = os.path.join(self.TEST_DIR, 'mecas2_signed.mdn') with open(received_file, 'rb') as fp: - in_message = as2.MDN() + in_message = as2.Mdn() in_message.parse(fp.read(), find_message_cb=self.find_message) def find_org(self, headers): From 1167781a24611ac3a4bd1e636e1c1a10a3babab7 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 22 Apr 2018 20:12:28 +0530 Subject: [PATCH 05/66] load si payload in binary --- tests/__init__.py | 2 +- tests/livetest_with_mecas2.py | 18 +++++++++--------- tests/livetest_with_oldpyas2.py | 18 +++++++++--------- tests/test_advanced.py | 12 +++++++----- tests/test_basic.py | 4 ++-- tests/test_mdn.py | 4 ++-- tests/test_with_mecas2.py | 4 ++-- 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 666ce31..943bbd9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,7 +6,7 @@ from pyas2lib import as2, exceptions -class PYAS2TestCase(unittest.TestCase): +class Pyas2TestCase(unittest.TestCase): TEST_DIR = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'fixtures') diff --git a/tests/livetest_with_mecas2.py b/tests/livetest_with_mecas2.py index 0e3c1c3..f33928b 100644 --- a/tests/livetest_with_mecas2.py +++ b/tests/livetest_with_mecas2.py @@ -1,16 +1,16 @@ from __future__ import unicode_literals, absolute_import, print_function -from . import as2, PYAS2TestCase +from . import as2, Pyas2TestCase import requests import os TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testdata') -class LiveTestMecAS2(PYAS2TestCase): +class LiveTestMecAS2(Pyas2TestCase): def setUp(self): self.org = as2.Organization( - as2_id='pyas2lib', + as2_name='pyas2lib', sign_key=self.private_key, sign_key_pass='test'.encode('utf-8'), decrypt_key=self.private_key, @@ -18,7 +18,7 @@ def setUp(self): ) self.partner = as2.Partner( - as2_id='mecas2', + as2_name='mecas2', verify_cert=self.mecas2_public_key, encrypt_cert=self.mecas2_public_key, mdn_mode=as2.SYNCHRONOUS_MDN, @@ -44,7 +44,7 @@ def test_compressed_message(self): raw_mdn = raw_mdn + '\n' + response.text - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( raw_mdn, find_message_cb=self.find_message) self.assertEqual(status, 'processed') @@ -67,7 +67,7 @@ def test_encrypted_message(self): raw_mdn = raw_mdn + '\n' + response.text - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( raw_mdn, find_message_cb=self.find_message) self.assertEqual(status, 'processed') @@ -90,7 +90,7 @@ def test_signed_message(self): raw_mdn += '{}: {}\n'.format(k, v) raw_mdn = raw_mdn + '\n' + response.text - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( raw_mdn, find_message_cb=self.find_message) self.assertEqual(status, 'processed') @@ -114,7 +114,7 @@ def test_encrypted_signed_message(self): raw_mdn += '{}: {}\n'.format(k, v) raw_mdn = raw_mdn + '\n' + response.text - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( raw_mdn, find_message_cb=self.find_message) self.assertEqual(status, 'processed') @@ -139,7 +139,7 @@ def test_encrypted_signed_compressed_message(self): raw_mdn += '{}: {}\n'.format(k, v) raw_mdn = raw_mdn + '\n' + response.text - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( raw_mdn, find_message_cb=self.find_message) self.assertEqual(status, 'processed') diff --git a/tests/livetest_with_oldpyas2.py b/tests/livetest_with_oldpyas2.py index 631f059..4a9fb64 100644 --- a/tests/livetest_with_oldpyas2.py +++ b/tests/livetest_with_oldpyas2.py @@ -1,16 +1,16 @@ from __future__ import unicode_literals, absolute_import, print_function -from . import as2, PYAS2TestCase +from . import as2, Pyas2TestCase import requests import os TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testdata') -class LiveTestMecAS2(PYAS2TestCase): +class LiveTestMecAS2(Pyas2TestCase): def setUp(self): self.org = as2.Organization( - as2_id='pyas2lib', + as2_name='pyas2lib', sign_key=self.private_key, sign_key_pass='test'.encode('utf-8'), decrypt_key=self.private_key, @@ -18,7 +18,7 @@ def setUp(self): ) self.partner = as2.Partner( - as2_id='pyas2idev', + as2_name='pyas2idev', verify_cert=self.oldpyas2_public_key, encrypt_cert=self.oldpyas2_public_key, mdn_mode=as2.SYNCHRONOUS_MDN, @@ -44,7 +44,7 @@ def test_compressed_message(self): raw_mdn = raw_mdn + '\n' + response.text - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( raw_mdn, find_message_cb=self.find_message) self.assertEqual(status, 'processed') @@ -67,7 +67,7 @@ def test_encrypted_message(self): raw_mdn = raw_mdn + '\n' + response.text - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( raw_mdn, find_message_cb=self.find_message) self.assertEqual(status, 'processed') @@ -90,7 +90,7 @@ def test_signed_message(self): raw_mdn += '{}: {}\n'.format(k, v) raw_mdn = raw_mdn + '\n' + response.text - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( raw_mdn, find_message_cb=self.find_message) self.assertEqual(status, 'processed') @@ -114,7 +114,7 @@ def test_encrypted_signed_message(self): raw_mdn += '{}: {}\n'.format(k, v) raw_mdn = raw_mdn + '\n' + response.text - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( raw_mdn, find_message_cb=self.find_message) self.assertEqual(status, 'processed') @@ -139,7 +139,7 @@ def test_encrypted_signed_compressed_message(self): raw_mdn += '{}: {}\n'.format(k, v) raw_mdn = raw_mdn + '\n' + response.text - out_mdn = as2.MDN() + out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( raw_mdn, find_message_cb=self.find_message) self.assertEqual(status, 'processed') diff --git a/tests/test_advanced.py b/tests/test_advanced.py index 8f01f40..b845cab 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals, absolute_import, print_function -from . import PYAS2TestCase, as2 +from . import Pyas2TestCase, as2 import os -class TestAdvanced(PYAS2TestCase): +class TestAdvanced(Pyas2TestCase): def setUp(self): self.org = as2.Organization( @@ -274,7 +274,7 @@ def find_message(self, message_id, message_recipient): return self.out_message -class SterlingIntegratorTest(PYAS2TestCase): +class SterlingIntegratorTest(Pyas2TestCase): def setUp(self): self.org = as2.Organization( @@ -294,7 +294,8 @@ def setUp(self): def xtest_process_message(self): """ Test processing message received from Sterling Integrator""" - with open(os.path.join(self.TEST_DIR, 'sb2bi_signed_cmp.msg')) as msg: + with open(os.path.join( + self.TEST_DIR, 'sb2bi_signed_cmp.msg'), 'rb') as msg: as2message = as2.Message() status, exception, as2mdn = as2message.parse( msg.read(), @@ -312,7 +313,8 @@ def test_process_mdn(self): as2mdn = as2.Mdn() # Parse the mdn and get the message status - with open(os.path.join(self.TEST_DIR, 'sb2bi_signed.mdn')) as mdn: + with open(os.path.join( + self.TEST_DIR, 'sb2bi_signed.mdn'), 'rb') as mdn: status, detailed_status = as2mdn.parse( mdn.read(), lambda x, y: message) self.assertEqual(status, 'processed') diff --git a/tests/test_basic.py b/tests/test_basic.py index 9486528..eed3840 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals, absolute_import, print_function -from . import as2, PYAS2TestCase +from . import as2, Pyas2TestCase -class TestBasic(PYAS2TestCase): +class TestBasic(Pyas2TestCase): def setUp(self): self.org = as2.Organization( diff --git a/tests/test_mdn.py b/tests/test_mdn.py index 70002c7..96aa5a9 100644 --- a/tests/test_mdn.py +++ b/tests/test_mdn.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals, absolute_import, print_function -from . import as2, PYAS2TestCase +from . import as2, Pyas2TestCase -class TestMDN(PYAS2TestCase): +class TestMDN(Pyas2TestCase): def setUp(self): diff --git a/tests/test_with_mecas2.py b/tests/test_with_mecas2.py index 75d5e86..3c48dd5 100644 --- a/tests/test_with_mecas2.py +++ b/tests/test_with_mecas2.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals, absolute_import, print_function -from . import PYAS2TestCase, as2 +from . import Pyas2TestCase, as2 import os -class TestMecAS2(PYAS2TestCase): +class TestMecAS2(Pyas2TestCase): def setUp(self): self.org = as2.Organization( From 35023f01358e03c616c94dacda87340b1139ffd3 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 22 Apr 2018 20:19:10 +0530 Subject: [PATCH 06/66] bump version --- .gitignore | 3 ++- pyas2lib/__init__.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index fea5f0f..70eeea2 100644 --- a/.gitignore +++ b/.gitignore @@ -90,4 +90,5 @@ ENV/ # IDEA .idea -.pytest_cache/ \ No newline at end of file +.pytest_cache/ +.coverage \ No newline at end of file diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 7d801e5..2d9da9b 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -1,9 +1,9 @@ from __future__ import absolute_import # from pyas2lib.as2 import DIGEST_ALGORITHMS, ENCRYPTION_ALGORITHMS,\ -# MDN_CONFIRM_TEXT, MDN_FAILED_TEXT, Partner, Organization, Message, MDN +# MDN_CONFIRM_TEXT, MDN_FAILED_TEXT, Partner, Organization, Message, Mdn import sys -VERSION = (1, 0, 0) +VERSION = (1, 0, 1) __version__ = '.'.join(map(str, VERSION)) @@ -16,7 +16,7 @@ # 'Partner', # 'Organization', # 'Message', - # 'MDN' + # 'Mdn' ] if (2, 7) <= sys.version_info < (3, 2): From ce397ea4488315a99922a194e2c73789198da802 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 1 May 2018 16:49:31 +0530 Subject: [PATCH 07/66] * Fix an issue with message decompression. * Add optional callback for checking duplicate messages in parse * Add test cases for decompression and duplicate errors * Add debug logging in parse and build --- CHANGELOG.md | 7 ++++ pyas2lib/__init__.py | 2 +- pyas2lib/as2.py | 53 +++++++++++++++++++++++------ pyas2lib/cms.py | 6 +--- tests/test_advanced.py | 77 ++++++++++++++++++++++++++++++++++++++---- 5 files changed, 122 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41276d7..62ae9bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Release History +## 1.0.2 - 2018-05-01 + +* Fix an issue with message decompression. +* Add optional callback for checking duplicate messages in parse +* Add test cases for decompression and duplicate errors +* Add debug logging in parse and build + ## 1.0.1 - 2018-04-22 * Check for incorrect passphrase when loading the private key. diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 2d9da9b..365ba5e 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -3,7 +3,7 @@ # MDN_CONFIRM_TEXT, MDN_FAILED_TEXT, Partner, Organization, Message, Mdn import sys -VERSION = (1, 0, 1) +VERSION = (1, 0, 2) __version__ = '.'.join(map(str, VERSION)) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 5cf6f8d..c93fcab 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -376,6 +376,8 @@ def build(self, data, filename=None, subject='AS2 Message', compress_message(canonicalize(self.payload))) encoders.encode_base64(compressed_message) self.payload = compressed_message + logger.debug('Compressed message %s payload as:\n%s' % ( + self.message_id, self.payload.as_string())) if self.receiver.sign: self.signed, self.digest_alg = True, self.receiver.digest_alg @@ -405,6 +407,9 @@ def build(self, data, filename=None, subject='AS2 Message', signed_message.attach(signature) self.payload = signed_message + logger.debug('Signed message %s payload as:\n%s' % ( + self.message_id, self.payload.as_string())) + if self.receiver.encrypt: self.encrypted, self.enc_alg = True, self.receiver.enc_alg encrypted_message = email_message.Message() @@ -421,6 +426,8 @@ def build(self, data, filename=None, subject='AS2 Message', )) encoders.encode_base64(encrypted_message) self.payload = encrypted_message + logger.debug('Encrypted message %s payload as:\n%s' % ( + self.message_id, self.payload.as_string())) if self.receiver.mdn_mode: as2_headers['disposition-notification-to'] = 'no-reply@pyas2.com' @@ -445,7 +452,8 @@ def build(self, data, filename=None, subject='AS2 Message', if self.payload.is_multipart(): self.payload.set_boundary(make_mime_boundary()) - def parse(self, raw_content, find_org_cb, find_partner_cb): + def parse(self, raw_content, find_org_cb, find_partner_cb, + find_message_cb=None): """Function parses the RAW AS2 message; decrypts, verifies and decompresses it and extracts the payload. @@ -460,6 +468,11 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): A callback the returns an Partner object if exists. The as2-from header value is passed as an argument to it. + :param find_message_cb: + An optional callback the returns an Message object if exists in + order to check for duplicates. The message id and partner id is + passed as arguments to it. + :return: A three element tuple containing (status, (exception, traceback) , mdn). The status is a string indicating the status of the @@ -493,6 +506,12 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): raise PartnerNotFound( 'Unknown AS2 partner with id {}'.format(partner_id)) + if find_message_cb and \ + find_message_cb(self.message_id, partner_id): + raise DuplicateDocument( + 'Duplicate message received, message with this ID ' + 'already processed.') + if self.sender.encrypt and \ self.payload.get_content_type() != 'application/pkcs7-mime': raise InsufficientSecurityError( @@ -500,11 +519,13 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): ' but encrypted message not found.'.format(partner_id)) if self.payload.get_content_type() == 'application/pkcs7-mime' \ - and self.payload.get_param( - 'smime-type') == 'enveloped-data': + and self.payload.get_param('smime-type') == 'enveloped-data': + encrypted_data = self.payload.get_payload(decode=True) + logger.debug(b'Decrypting the payload :\n%s' % encrypted_data) + self.encrypted = True self.enc_alg, decrypted_content = decrypt_message( - self.payload.get_payload(decode=True), + encrypted_data, self.receiver.decrypt_key ) raw_content = decrypted_content @@ -522,6 +543,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): 'but signed message not found.'.format(partner_id)) if self.payload.get_content_type() == 'multipart/signed': + logger.debug(b'Verifying the signed payload:\n{0:s}'.format( + self.payload.as_string())) self.signed = True signature = None message_boundary = ( @@ -540,7 +563,6 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): self.digest_alg = verify_message( mic_content, signature, verify_cert) except IntegrityError: - print(self.payload.is_multipart()) mic_content = raw_content.split(message_boundary)[ 1].replace(b'\n', b'\r\n') self.digest_alg = verify_message( @@ -552,11 +574,14 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): self.mic = binascii.b2a_base64(digest_func.digest()).strip() if self.payload.get_content_type() == 'application/pkcs7-mime' \ - and self.payload.get_param( - 'smime-type') == 'compressed-data': + and self.payload.get_param('smime-type') == 'compressed-data': + + compressed_data = self.payload.get_payload(decode=True) + logger.debug( + b'Decompressing the payload:\n%s' % compressed_data) + self.compressed = True - decompressed_data = decompress_message( - self.payload.get_payload(decode=True)) + decompressed_data = decompress_message(compressed_data) self.payload = parse_mime(decompressed_data) except Exception as e: @@ -567,7 +592,7 @@ def parse(self, raw_content, find_org_cb, find_partner_cb): finally: # Update the payload headers with the original headers for k, v in as2_headers.items(): - if self.payload.get(k): + if self.payload.get(k) and k.lower() != 'content-disposition': del self.payload[k] self.payload.add_header(k, v) @@ -708,6 +733,9 @@ def build(self, message, status, detailed_status=None): encoders.encode_7or8bit(mdn_base) self.payload.attach(mdn_base) + logger.debug('MDN for message %s created:\n%s' % ( + message.message_id, mdn_base.as_string())) + # Sign the MDN if it is requested by the sender if message.headers.get('disposition-notification-options') and \ message.receiver and message.receiver.sign_key: @@ -733,6 +761,8 @@ def build(self, message, status, detailed_status=None): message.receiver.sign_key )) encoders.encode_base64(signature) + logger.debug( + 'Signature for MDN created:\n%s' % signature.as_string()) signed_mdn.set_param('micalg', self.digest_alg) signed_mdn.attach(signature) @@ -811,6 +841,9 @@ def parse(self, raw_content, find_message_cb): for part in self.payload.walk(): if part.get_content_type() == 'message/disposition-notification': + logger.debug('Found MDN report for message %s:\n%s' % ( + orig_message.message_id, part.as_string())) + mdn = part.get_payload()[-1] mdn_status = mdn['Disposition'].split( ';').pop().strip().split(':') diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index e454951..cde56a4 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -57,12 +57,11 @@ def decompress_message(compressed_data): :return: A byte string containing the decompressed original message. """ - decompressed_content = '' try: cms_content = cms.ContentInfo.load(compressed_data) if cms_content['content_type'].native == 'compressed_data': - decompressed_content = cms_content['content'].decompressed + return cms_content['content'].decompressed else: raise DecompressionError('Compressed data not found in ASN.1 ') @@ -70,9 +69,6 @@ def decompress_message(compressed_data): raise DecompressionError( 'Decompression failed with cause: {}'.format(e)) - finally: - return decompressed_content - def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): """Function encrypts data and returns the generated ASN.1 diff --git a/tests/test_advanced.py b/tests/test_advanced.py index b845cab..7024982 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -42,7 +42,8 @@ def test_binary_message(self): in_mic_content = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, + find_message_cb=lambda x, y: False ) # Compare the mic contents of the input and output messages @@ -69,7 +70,8 @@ def test_partner_not_found(self): _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_none + find_partner_cb=self.find_none, + find_message_cb=lambda x, y: False ) out_mdn = as2.Mdn() @@ -85,7 +87,8 @@ def test_partner_not_found(self): _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_none, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, + find_message_cb=lambda x, y: False ) out_mdn = as2.Mdn() @@ -96,6 +99,62 @@ def test_partner_not_found(self): self.assertEqual(status, 'processed/Error') self.assertEqual(detailed_status, 'unknown-trading-partner') + def test_duplicate_message(self): + """ Test case where a duplicate message is sent to the partner """ + + # Build an As2 message to be transmitted to partner + self.partner.sign = True + self.partner.encrypt = True + self.partner.mdn_mode = as2.SYNCHRONOUS_MDN + self.out_message = as2.Message(self.org, self.partner) + self.out_message.build(self.test_data) + + # Parse the generated AS2 message as the partner + raw_out_message = \ + self.out_message.headers_str + b'\r\n' + self.out_message.content + in_message = as2.Message() + _, _, mdn = in_message.parse( + raw_out_message, + find_org_cb=self.find_org, + find_partner_cb=self.find_partner, + find_message_cb=lambda x, y: True + ) + + out_mdn = as2.Mdn() + status, detailed_status = out_mdn.parse( + mdn.headers_str + b'\r\n' + mdn.content, + find_message_cb=self.find_message + ) + self.assertEqual(status, 'processed/Warning') + self.assertEqual(detailed_status, 'duplicate-document') + + def test_failed_decompression(self): + """ Test case where message decompression has failed """ + + # Build an As2 message to be transmitted to partner + self.partner.compress = True + self.partner.mdn_mode = as2.SYNCHRONOUS_MDN + self.out_message = as2.Message(self.org, self.partner) + self.out_message.build(self.test_data) + + # Parse the generated AS2 message as the partner + raw_out_message = \ + self.out_message.headers_str + b'\r\n' + 'xxxxx' + in_message = as2.Message() + _, exec_info, mdn = in_message.parse( + raw_out_message, + find_org_cb=self.find_org, + find_partner_cb=self.find_partner, + ) + + out_mdn = as2.Mdn() + status, detailed_status = out_mdn.parse( + mdn.headers_str + b'\r\n' + mdn.content, + find_message_cb=self.find_message + ) + self.assertEqual(status, 'processed/Error') + self.assertEqual(detailed_status, 'decompression-failed') + def test_insufficient_security(self): """ Test case where message security is not as per the configuration """ @@ -113,7 +172,8 @@ def test_insufficient_security(self): _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, + find_message_cb=lambda x, y: False ) out_mdn = as2.Mdn() @@ -142,7 +202,8 @@ def test_failed_decryption(self): _, exec_info, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, + find_message_cb=lambda x, y: False ) out_mdn = as2.Mdn() @@ -171,7 +232,8 @@ def test_failed_signature(self): _, exec_info, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, + find_message_cb=lambda x, y: False ) out_mdn = as2.Mdn() @@ -300,7 +362,8 @@ def xtest_process_message(self): status, exception, as2mdn = as2message.parse( msg.read(), lambda x: self.org, - lambda y: self.partner + lambda y: self.partner, + lambda x, y: False ) print(status, exception, as2mdn) self.assertEqual(status, 'processed') From 9f4fefdcd8d89e5bf89e0295e14f959a0ec24a9c Mon Sep 17 00:00:00 2001 From: Abhishek Ram Date: Tue, 1 May 2018 12:01:06 +0000 Subject: [PATCH 08/66] remove debug logging for now --- CHANGELOG.md | 1 - pyas2lib/as2.py | 36 +++++++++++++++++++----------------- setup.py | 3 ++- tests/test_advanced.py | 3 ++- tests/test_basic.py | 1 - 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62ae9bd..b32a4cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ * Fix an issue with message decompression. * Add optional callback for checking duplicate messages in parse * Add test cases for decompression and duplicate errors -* Add debug logging in parse and build ## 1.0.1 - 2018-04-22 diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index c93fcab..38ab82b 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -376,8 +376,8 @@ def build(self, data, filename=None, subject='AS2 Message', compress_message(canonicalize(self.payload))) encoders.encode_base64(compressed_message) self.payload = compressed_message - logger.debug('Compressed message %s payload as:\n%s' % ( - self.message_id, self.payload.as_string())) + # logger.debug(b'Compressed message %s payload as:\n%s' % ( + # self.message_id, self.payload.as_string())) if self.receiver.sign: self.signed, self.digest_alg = True, self.receiver.digest_alg @@ -407,8 +407,8 @@ def build(self, data, filename=None, subject='AS2 Message', signed_message.attach(signature) self.payload = signed_message - logger.debug('Signed message %s payload as:\n%s' % ( - self.message_id, self.payload.as_string())) + # logger.debug(b'Signed message %s payload as:\n%s' % ( + # self.message_id, self.payload.as_string())) if self.receiver.encrypt: self.encrypted, self.enc_alg = True, self.receiver.enc_alg @@ -426,8 +426,8 @@ def build(self, data, filename=None, subject='AS2 Message', )) encoders.encode_base64(encrypted_message) self.payload = encrypted_message - logger.debug('Encrypted message %s payload as:\n%s' % ( - self.message_id, self.payload.as_string())) + # logger.debug(b'Encrypted message %s payload as:\n%s' % ( + # self.message_id, self.payload.as_string())) if self.receiver.mdn_mode: as2_headers['disposition-notification-to'] = 'no-reply@pyas2.com' @@ -521,7 +521,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, if self.payload.get_content_type() == 'application/pkcs7-mime' \ and self.payload.get_param('smime-type') == 'enveloped-data': encrypted_data = self.payload.get_payload(decode=True) - logger.debug(b'Decrypting the payload :\n%s' % encrypted_data) + # logger.debug( + # 'Decrypting the payload :\n%s' % self.payload.as_string()) self.encrypted = True self.enc_alg, decrypted_content = decrypt_message( @@ -543,8 +544,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, 'but signed message not found.'.format(partner_id)) if self.payload.get_content_type() == 'multipart/signed': - logger.debug(b'Verifying the signed payload:\n{0:s}'.format( - self.payload.as_string())) + # logger.debug(b'Verifying the signed payload:\n{0:s}'.format( + # self.payload.as_string())) self.signed = True signature = None message_boundary = ( @@ -577,8 +578,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, and self.payload.get_param('smime-type') == 'compressed-data': compressed_data = self.payload.get_payload(decode=True) - logger.debug( - b'Decompressing the payload:\n%s' % compressed_data) + # logger.debug( + # b'Decompressing the payload:\n%s' % compressed_data) self.compressed = True decompressed_data = decompress_message(compressed_data) @@ -588,6 +589,7 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, status = getattr(e, 'disposition_type', 'processed/Error') detailed_status = getattr( e, 'disposition_modifier', 'unexpected-processing-error') + print(traceback.format_exc()) exception = (e, traceback.format_exc()) finally: # Update the payload headers with the original headers @@ -733,8 +735,8 @@ def build(self, message, status, detailed_status=None): encoders.encode_7or8bit(mdn_base) self.payload.attach(mdn_base) - logger.debug('MDN for message %s created:\n%s' % ( - message.message_id, mdn_base.as_string())) + # logger.debug('MDN for message %s created:\n%s' % ( + # message.message_id, mdn_base.as_string())) # Sign the MDN if it is requested by the sender if message.headers.get('disposition-notification-options') and \ @@ -761,8 +763,8 @@ def build(self, message, status, detailed_status=None): message.receiver.sign_key )) encoders.encode_base64(signature) - logger.debug( - 'Signature for MDN created:\n%s' % signature.as_string()) + # logger.debug( + # 'Signature for MDN created:\n%s' % signature.as_string()) signed_mdn.set_param('micalg', self.digest_alg) signed_mdn.attach(signature) @@ -841,8 +843,8 @@ def parse(self, raw_content, find_message_cb): for part in self.payload.walk(): if part.get_content_type() == 'message/disposition-notification': - logger.debug('Found MDN report for message %s:\n%s' % ( - orig_message.message_id, part.as_string())) + # logger.debug('Found MDN report for message %s:\n%s' % ( + # orig_message.message_id, part.as_string())) mdn = part.get_payload()[-1] mdn_status = mdn['Disposition'].split( diff --git a/setup.py b/setup.py index ab52b17..0c20326 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,8 @@ tests_require = [ 'pytest==3.4.0', 'pytest-cov==2.5.1', - 'coverage==4.3.4' + 'coverage==4.3.4', + 'nose', ] setup( diff --git a/tests/test_advanced.py b/tests/test_advanced.py index 7024982..cdc69df 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals, absolute_import, print_function from . import Pyas2TestCase, as2 import os +import base64 class TestAdvanced(Pyas2TestCase): @@ -139,7 +140,7 @@ def test_failed_decompression(self): # Parse the generated AS2 message as the partner raw_out_message = \ - self.out_message.headers_str + b'\r\n' + 'xxxxx' + self.out_message.headers_str + b'\r\n' + base64.b64encode(b'xxxxx') in_message = as2.Message() _, exec_info, mdn = in_message.parse( raw_out_message, diff --git a/tests/test_basic.py b/tests/test_basic.py index eed3840..c2f9401 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -91,7 +91,6 @@ def test_signed_message(self): # Parse the generated AS2 message as the partner in_message = as2.Message() - print(raw_out_message) in_message.parse( raw_out_message, find_org_cb=self.find_org, From 04179e21cd3343769cf501def94c932006fedc2e Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 1 May 2018 19:02:35 +0530 Subject: [PATCH 09/66] * Remove unnecessary conversions to bytes. --- CHANGELOG.md | 4 ++++ pyas2lib/as2.py | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b32a4cd..98f9932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## 1.0.3 - 2018-05-01 + +* Remove unnecessary conversions to bytes. + ## 1.0.2 - 2018-05-01 * Fix an issue with message decompression. diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 38ab82b..65a5873 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -64,10 +64,10 @@ def __init__(self, as2_name, sign_key=None, sign_key_pass=None, asynchronous MDNs. """ self.sign_key = self.load_key( - sign_key, byte_cls(sign_key_pass)) if sign_key else None + sign_key, sign_key_pass) if sign_key else None self.decrypt_key = self.load_key( - decrypt_key, byte_cls(decrypt_key_pass)) if decrypt_key else None + decrypt_key, decrypt_key_pass) if decrypt_key else None self.as2_name = as2_name self.mdn_url = mdn_url @@ -79,7 +79,7 @@ def load_key(key_str, key_pass): try: # First try to parse as a p12 file - key, cert, _ = asymmetric.load_pkcs12(key_str, byte_cls(key_pass)) + key, cert, _ = asymmetric.load_pkcs12(key_str, key_pass) except ValueError as e: # If it fails due to invalid password raise error here if e.args[0] == 'Password provided is invalid': @@ -92,8 +92,7 @@ def load_key(key_str, key_pass): cert = asymmetric.load_certificate(kc) except (ValueError, TypeError): try: - key = asymmetric.load_private_key(kc, - byte_cls(key_pass)) + key = asymmetric.load_private_key(kc, key_pass) except OSError: raise AS2Exception( 'Invalid Private Key or password is not correct.') From 5f24923f3114a5ca04b4c8379a773b01a74f51fa Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 1 May 2018 19:05:27 +0530 Subject: [PATCH 10/66] Bump version --- pyas2lib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 365ba5e..2b8876b 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -3,7 +3,7 @@ # MDN_CONFIRM_TEXT, MDN_FAILED_TEXT, Partner, Organization, Message, Mdn import sys -VERSION = (1, 0, 2) +VERSION = (1, 0, 3) __version__ = '.'.join(map(str, VERSION)) From 6af6bc71fe8a8cfb3465dad82ecc50539e3fd551 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Fri, 26 Apr 2019 09:23:22 +0530 Subject: [PATCH 11/66] check for compression before signatures as well --- pyas2lib/as2.py | 26 ++++++++++++++++---------- pyas2lib/cms.py | 1 - 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 65a5873..722ace5 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -448,9 +448,19 @@ def build(self, data, filename=None, subject='AS2 Message', self.payload.replace_header(k, v) else: self.payload.add_header(k, v) + if self.payload.is_multipart(): self.payload.set_boundary(make_mime_boundary()) + @staticmethod + def decompress_data(payload): + if payload.get_content_type() == 'application/pkcs7-mime' \ + and payload.get_param('smime-type') == 'compressed-data': + compressed_data = payload.get_payload(decode=True) + decompressed_data = decompress_message(compressed_data) + return True, parse_mime(decompressed_data) + return False, payload + def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None): """Function parses the RAW AS2 message; decrypts, verifies and @@ -536,6 +546,9 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, self.payload.set_payload(decrypted_content) self.payload.set_type('application/edi-consent') + # Check for compressed data here + self.compressed, self.payload = self.decompress_data(self.payload) + if self.sender.sign and \ self.payload.get_content_type() != 'multipart/signed': raise InsufficientSecurityError( @@ -573,16 +586,9 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, digest_func.update(mic_content) self.mic = binascii.b2a_base64(digest_func.digest()).strip() - if self.payload.get_content_type() == 'application/pkcs7-mime' \ - and self.payload.get_param('smime-type') == 'compressed-data': - - compressed_data = self.payload.get_payload(decode=True) - # logger.debug( - # b'Decompressing the payload:\n%s' % compressed_data) - - self.compressed = True - decompressed_data = decompress_message(compressed_data) - self.payload = parse_mime(decompressed_data) + # Check for compressed data here + if not self.compressed: + self.compressed, self.payload = self.decompress_data(self.payload) except Exception as e: status = getattr(e, 'disposition_type', 'processed/Error') diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index cde56a4..3bb99df 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -59,7 +59,6 @@ def decompress_message(compressed_data): """ try: cms_content = cms.ContentInfo.load(compressed_data) - if cms_content['content_type'].native == 'compressed_data': return cms_content['content'].decompressed else: From dd883b318a6a054ac4f01d37189345f1104f23ae Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 30 Apr 2019 12:16:58 +0530 Subject: [PATCH 12/66] add support for additional algorithms --- pyas2lib/cms.py | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 3bb99df..7320c08 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -20,6 +20,9 @@ 'tripledes_192_cbc', 'rc2_128_cbc', 'rc4_128_cbc' + 'aes_128_cbc', + 'aes_192_cbc', + 'aes_256_cbc', ) @@ -86,14 +89,39 @@ def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): enc_alg_asn1, key, encrypted_content = None, None, None # Generate the symmetric encryption key and encrypt the message + key = util.rand_bytes(int(key_length) // 8) + algorithm_id = None + iv, encrypted_content = None, None + if cipher == 'tripledes': - key = util.rand_bytes(int(key_length)//8) + algorithm_id = '1.2.840.113549.3.7' iv, encrypted_content = symmetric.tripledes_cbc_pkcs5_encrypt( key, data_to_encrypt, None) - enc_alg_asn1 = algos.EncryptionAlgorithm({ - 'algorithm': algos.EncryptionAlgorithmId('tripledes_3key'), - 'parameters': cms.OctetString(iv) - }) + + elif cipher == 'rc2': + algorithm_id = '1.2.840.113549.3.2' + iv, encrypted_content = symmetric.rc2_cbc_pkcs5_encrypt( + key, data_to_encrypt, None) + + elif cipher == 'rc4': + algorithm_id = '1.2.840.113549.3.4' + encrypted_content = symmetric.rc4_encrypt(key, data_to_encrypt) + + elif cipher == 'aes': + if key_length == '128': + algorithm_id = '2.16.840.1.101.3.4.1.2' + elif key_length == '192': + algorithm_id = '2.16.840.1.101.3.4.1.22' + elif key_length == '256': + algorithm_id = '2.16.840.1.101.3.4.1.42' + + iv, encrypted_content = symmetric.aes_cbc_pkcs7_encrypt( + key, data_to_encrypt, None) + + enc_alg_asn1 = algos.EncryptionAlgorithm({ + 'algorithm': algorithm_id, + 'parameters': cms.OctetString(iv) + }) # Encrypt the key and build the ASN.1 message encrypted_key = asymmetric.rsa_pkcs1v15_encrypt(encryption_cert, key) @@ -167,6 +195,15 @@ def decrypt_message(encrypted_data, decryption_key): cipher = 'tripledes_192_cbc' decrypted_content = symmetric.tripledes_cbc_pkcs5_decrypt( key, encapsulated_data, alg.encryption_iv) + elif alg.encryption_cipher == 'aes': + decrypted_content = symmetric.aes_cbc_pkcs7_decrypt( + key, encapsulated_data, alg.encryption_iv) + elif alg.encryption_cipher == 'rc4': + decrypted_content = symmetric.rc2_cbc_pkcs5_decrypt( + key, encapsulated_data, alg.encryption_iv) + elif alg.encryption_cipher == 'rc2': + decrypted_content = symmetric.rc2_cbc_pkcs5_encrypt( + key, encapsulated_data, alg.encryption_iv) else: raise AS2Exception('Unsupported Encryption Algorithm') except Exception as e: From 7fac38aab7bbf6df1be332bdf1ea61fe60e3595e Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 30 Apr 2019 12:39:41 +0530 Subject: [PATCH 13/66] remove support for python 2 --- .travis.yml | 3 +- CHANGELOG.md | 6 +++ pyas2lib/as2.py | 57 +++++++++++++++++---------- pyas2lib/cms.py | 15 ++++--- pyas2lib/compat.py | 88 ------------------------------------------ pyas2lib/utils.py | 13 ++++--- setup.py | 3 +- tests/test_advanced.py | 4 +- tests/test_basic.py | 3 +- tox.ini | 2 +- 10 files changed, 63 insertions(+), 131 deletions(-) delete mode 100644 pyas2lib/compat.py diff --git a/.travis.yml b/.travis.yml index f6dae0a..3349ac0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: python python: - - '2.7' - - '3.4' - '3.5' - '3.6' + - '3.7' install: - python setup.py install - pip install pytest-cov diff --git a/CHANGELOG.md b/CHANGELOG.md index 98f9932..66ac4e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 1.0.4 - 2019-04-26 + +* Handle cases where compression is done before signing. +* Add support for additional encryption algorithms +* Remove support for Python 2. + ## 1.0.3 - 2018-05-01 * Remove unnecessary conversions to bytes. diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 722ace5..d394900 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -1,21 +1,32 @@ -from __future__ import absolute_import, unicode_literals -from .compat import str_cls, byte_cls, parse_mime -from .cms import compress_message, decompress_message, decrypt_message, \ - encrypt_message, verify_message, sign_message -from .cms import DIGEST_ALGORITHMS, ENCRYPTION_ALGORITHMS -from .utils import canonicalize, mime_to_bytes, quote_as2name, unquote_as2name, \ - make_mime_boundary, extract_first_part, pem_to_der, split_pem, \ - verify_certificate_chain -from .exceptions import * -from email import utils as email_utils -from email import message as email_message -from email import encoders -from email.mime.multipart import MIMEMultipart -from oscrypto import asymmetric import logging import hashlib import binascii import traceback +from email import encoders +from email import message as email_message +from email import message_from_bytes as parse_mime +from email import utils as email_utils +from email.mime.multipart import MIMEMultipart +from oscrypto import asymmetric + +from pyas2lib.cms import DIGEST_ALGORITHMS +from pyas2lib.cms import ENCRYPTION_ALGORITHMS +from pyas2lib.cms import compress_message +from pyas2lib.cms import decompress_message +from pyas2lib.cms import decrypt_message +from pyas2lib.cms import encrypt_message +from pyas2lib.cms import sign_message +from pyas2lib.cms import verify_message +from pyas2lib.exceptions import * +from pyas2lib.utils import canonicalize +from pyas2lib.utils import extract_first_part +from pyas2lib.utils import make_mime_boundary +from pyas2lib.utils import mime_to_bytes +from pyas2lib.utils import pem_to_der +from pyas2lib.utils import quote_as2name +from pyas2lib.utils import split_pem +from pyas2lib.utils import unquote_as2name +from pyas2lib.utils import verify_certificate_chain logger = logging.getLogger('pyas2lib') @@ -275,7 +286,7 @@ def content(self): return boundary + boundary.join(temp) else: content = self.payload.get_payload() - if isinstance(content, str_cls): + if isinstance(content, str): content = content.encode('utf-8') return content @@ -319,8 +330,8 @@ def build(self, data, filename=None, subject='AS2 Message', """ # Validations - assert type(data) is byte_cls, \ - 'Parameter data must be of type {}'.format(byte_cls) + assert type(data) is bytes, \ + 'Parameter data must be of bytes type.' additional_headers = additional_headers if additional_headers else {} assert type(additional_headers) is dict @@ -371,10 +382,13 @@ def build(self, data, filename=None, subject='AS2 Message', compressed_message.set_param('smime-type', 'compressed-data') compressed_message.add_header( 'Content-Disposition', 'attachment', filename='smime.p7z') + # compressed_message['Content-Transfer-Encoding'] = 'binary' compressed_message.set_payload( compress_message(canonicalize(self.payload))) + encoders.encode_base64(compressed_message) self.payload = compressed_message + # logger.debug(b'Compressed message %s payload as:\n%s' % ( # self.message_id, self.payload.as_string())) @@ -402,6 +416,7 @@ def build(self, data, filename=None, subject='AS2 Message', signature.set_payload(sign_message( mic_content, self.digest_alg, self.sender.sign_key)) encoders.encode_base64(signature) + signed_message.set_param('micalg', self.digest_alg) signed_message.attach(signature) self.payload = signed_message @@ -459,6 +474,7 @@ def decompress_data(payload): compressed_data = payload.get_payload(decode=True) decompressed_data = decompress_message(compressed_data) return True, parse_mime(decompressed_data) + return False, payload def parse(self, raw_content, find_org_cb, find_partner_cb, @@ -535,9 +551,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, self.encrypted = True self.enc_alg, decrypted_content = decrypt_message( - encrypted_data, - self.receiver.decrypt_key - ) + encrypted_data, self.receiver.decrypt_key) + raw_content = decrypted_content self.payload = parse_mime(decrypted_content) @@ -594,8 +609,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, status = getattr(e, 'disposition_type', 'processed/Error') detailed_status = getattr( e, 'disposition_modifier', 'unexpected-processing-error') - print(traceback.format_exc()) exception = (e, traceback.format_exc()) + logger.error('Failed to parse AS2 message\n: %s' % traceback.format_exc()) finally: # Update the payload headers with the original headers for k, v in as2_headers.items(): diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 7320c08..080e594 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -1,12 +1,11 @@ -from __future__ import absolute_import, unicode_literals -from asn1crypto import cms, core, algos -from oscrypto import asymmetric, symmetric, util -from datetime import datetime -from collections import OrderedDict -from .compat import byte_cls -from .exceptions import * import hashlib import zlib +from asn1crypto import cms, core, algos +from collections import OrderedDict +from datetime import datetime +from oscrypto import asymmetric, symmetric, util + +from pyas2lib.exceptions import * DIGEST_ALGORITHMS = ( 'md5', @@ -378,7 +377,7 @@ def verify_message(data_to_verify, signature, verify_cert): for attr in signed_attributes.native: attr_dict[attr['type']] = attr['values'] - message_digest = byte_cls() + message_digest = bytes() for d in attr_dict['message_digest']: message_digest += d diff --git a/pyas2lib/compat.py b/pyas2lib/compat.py deleted file mode 100644 index 8535022..0000000 --- a/pyas2lib/compat.py +++ /dev/null @@ -1,88 +0,0 @@ -import sys - -# Syntax sugar. -_ver = sys.version_info - -#: Python 2.x? -is_py2 = (_ver[0] == 2) - -#: Python 3.x? -is_py3 = (_ver[0] == 3) - - -if is_py2: - str_cls = unicode # noqa - byte_cls = str - int_types = (int, long) # noqa - from email import message_from_string as parse_mime - from cStringIO import StringIO - from cStringIO import StringIO as BytesIO - from email.generator import Generator, NL, fcre, _make_boundary - - class BytesGenerator(Generator): - - def _handle_multipart(self, msg): - # The trick here is to write out each part separately, merge the all - # together, and then make sure that the boundary we've chosen isn't - # present in the payload. - msgtexts = [] - subparts = msg.get_payload() - if subparts is None: - subparts = [] - elif isinstance(subparts, basestring): - # e.g. a non-strict parse of a message with no starting boundary - self._fp.write(subparts) - return - elif not isinstance(subparts, list): - # Scalar payload - subparts = [subparts] - for part in subparts: - s = StringIO() - g = self.clone(s) - g.flatten(part, unixfrom=False) - msgtexts.append(s.getvalue()) - # BAW: What about boundaries that are wrapped in double-quotes? - boundary = msg.get_boundary() - if not boundary: - # Create a boundary that doesn't appear in any of the - # message texts. - alltext = NL.join(msgtexts) - boundary = _make_boundary(alltext) - msg.set_boundary(boundary) - # If there's a preamble, write it out, with a trailing CRLF - if msg.preamble is not None: - if self._mangle_from_: - preamble = fcre.sub('>From ', msg.preamble) - else: - preamble = msg.preamble - print >> self._fp, preamble - # dash-boundary transport-padding CRLF - print >> self._fp, '--' + boundary - # body-part - if msgtexts: - self._fp.write(msgtexts.pop(0)) - # *encapsulation - # --> delimiter transport-padding - # --> CRLF body-part - for body_part in msgtexts: - # delimiter transport-padding CRLF - print >> self._fp, '\n--' + boundary - # body-part - self._fp.write(body_part) - # close-delimiter transport-padding - self._fp.write('\n--' + boundary + '--' + NL) - if msg.epilogue is not None: - if self._mangle_from_: - epilogue = fcre.sub('>From ', msg.epilogue) - else: - epilogue = msg.epilogue - self._fp.write(epilogue) - -elif is_py3: - str_cls = str - byte_cls = bytes - int_types = int - from email import message_from_bytes as parse_mime - from io import StringIO - from io import BytesIO - from email.generator import Generator, BytesGenerator diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index b4d3f91..0de6632 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -1,12 +1,13 @@ -from __future__ import absolute_import, unicode_literals -from .compat import BytesIO, BytesGenerator, is_py2, _ver -from .exceptions import AS2Exception -from OpenSSL import crypto -from asn1crypto import pem import email +import random import re import sys -import random +from OpenSSL import crypto +from asn1crypto import pem +from email.generator import BytesGenerator +from io import BytesIO + +from pyas2lib.exceptions import AS2Exception def unquote_as2name(quoted_name): diff --git a/setup.py b/setup.py index 0c20326..8c5502a 100644 --- a/setup.py +++ b/setup.py @@ -37,10 +37,9 @@ "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Topic :: Security :: Cryptography", "Topic :: Communications", diff --git a/tests/test_advanced.py b/tests/test_advanced.py index cdc69df..b3e51ec 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -40,7 +40,7 @@ def test_binary_message(self): # Parse the generated AS2 message as the partner in_message = as2.Message() - in_mic_content = in_message.parse( + status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner, @@ -50,6 +50,7 @@ def test_binary_message(self): # Compare the mic contents of the input and output messages # self.assertEqual(original_message, # in_message.payload.get_payload(decode=True)) + self.assertEqual(status, 'processed') self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertEqual(out_message.mic, in_message.mic) @@ -366,7 +367,6 @@ def xtest_process_message(self): lambda y: self.partner, lambda x, y: False ) - print(status, exception, as2mdn) self.assertEqual(status, 'processed') def test_process_mdn(self): diff --git a/tests/test_basic.py b/tests/test_basic.py index c2f9401..578cee8 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -141,13 +141,14 @@ def test_encrypted_signed_compressed_message(self): # Parse the generated AS2 message as the partner in_message = as2.Message() - in_message.parse( + status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare the mic contents of the input and output messages + self.assertEqual(status, 'processed') self.assertEqual( self.test_data.replace(b'\n', b'\r\n'), in_message.content) self.assertTrue(in_message.signed) diff --git a/tox.ini b/tox.ini index 87dbbfa..a4875d6 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py34, py35, py36 +envlist = py35, py36, py37 [testenv] commands = {envpython} setup.py test From 0fcf0701a270458954dc8f46188a1d9d3b92208f Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 30 Apr 2019 15:42:24 +0530 Subject: [PATCH 14/66] use binary encoding for encryption and signatures --- CHANGELOG.md | 3 +- pyas2lib/as2.py | 69 ++++++++++++++++++++++----------------------- pyas2lib/cms.py | 13 ++++----- pyas2lib/utils.py | 25 +++++++++++----- tests/test_basic.py | 32 +++++++++++---------- 5 files changed, 76 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ac4e1..056a904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## 1.0.4 - 2019-04-26 * Handle cases where compression is done before signing. -* Add support for additional encryption algorithms +* Add support for additional encryption algorithms. +* Use binary encoding for encryption and signatures. * Remove support for Python 2. ## 1.0.3 - 2018-05-01 diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index d394900..3e5a71a 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -278,14 +278,13 @@ def content(self): return '' if self.payload.is_multipart(): - message_bytes = mime_to_bytes( - self.payload, 0).replace(b'\n', b'\r\n') + message_bytes = mime_to_bytes(self.payload, 0) boundary = b'--' + self.payload.get_boundary().encode('utf-8') temp = message_bytes.split(boundary) temp.pop(0) return boundary + boundary.join(temp) else: - content = self.payload.get_payload() + content = self.payload.get_payload(decode=True) if isinstance(content, str): content = content.encode('utf-8') return content @@ -382,15 +381,15 @@ def build(self, data, filename=None, subject='AS2 Message', compressed_message.set_param('smime-type', 'compressed-data') compressed_message.add_header( 'Content-Disposition', 'attachment', filename='smime.p7z') - # compressed_message['Content-Transfer-Encoding'] = 'binary' + compressed_message.add_header( + 'Content-Transfer-Encoding', 'binary') compressed_message.set_payload( - compress_message(canonicalize(self.payload))) + compress_message(mime_to_bytes(self.payload, 0))) - encoders.encode_base64(compressed_message) self.payload = compressed_message - # logger.debug(b'Compressed message %s payload as:\n%s' % ( - # self.message_id, self.payload.as_string())) + logger.debug('Compressed message %s payload as:\n%s' % ( + self.message_id, self.payload.as_string())) if self.receiver.sign: self.signed, self.digest_alg = True, self.receiver.digest_alg @@ -421,8 +420,8 @@ def build(self, data, filename=None, subject='AS2 Message', signed_message.attach(signature) self.payload = signed_message - # logger.debug(b'Signed message %s payload as:\n%s' % ( - # self.message_id, self.payload.as_string())) + logger.debug('Signed message %s payload as:\n%s' % ( + self.message_id, mime_to_bytes(self.payload, 0))) if self.receiver.encrypt: self.encrypted, self.enc_alg = True, self.receiver.enc_alg @@ -432,16 +431,14 @@ def build(self, data, filename=None, subject='AS2 Message', encrypted_message.set_param('smime-type', 'enveloped-data') encrypted_message.add_header( 'Content-Disposition', 'attachment', filename='smime.p7m') + encrypted_message.add_header('Content-Transfer-Encoding', 'binary') encrypt_cert = self.receiver.load_encrypt_cert() encrypted_message.set_payload(encrypt_message( - canonicalize(self.payload), - self.enc_alg, - encrypt_cert - )) - encoders.encode_base64(encrypted_message) + mime_to_bytes(self.payload, 0), self.enc_alg, encrypt_cert)) + self.payload = encrypted_message - # logger.debug(b'Encrypted message %s payload as:\n%s' % ( - # self.message_id, self.payload.as_string())) + logger.debug('Encrypted message %s payload as:\n%s' % ( + self.message_id, self.payload.as_string())) if self.receiver.mdn_mode: as2_headers['disposition-notification-to'] = 'no-reply@pyas2.com' @@ -468,7 +465,7 @@ def build(self, data, filename=None, subject='AS2 Message', self.payload.set_boundary(make_mime_boundary()) @staticmethod - def decompress_data(payload): + def _decompress_data(payload): if payload.get_content_type() == 'application/pkcs7-mime' \ and payload.get_param('smime-type') == 'compressed-data': compressed_data = payload.get_payload(decode=True) @@ -562,7 +559,7 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, self.payload.set_type('application/edi-consent') # Check for compressed data here - self.compressed, self.payload = self.decompress_data(self.payload) + self.compressed, self.payload = self._decompress_data(self.payload) if self.sender.sign and \ self.payload.get_content_type() != 'multipart/signed': @@ -575,8 +572,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, # self.payload.as_string())) self.signed = True signature = None - message_boundary = ( - '--' + self.payload.get_boundary()).encode('utf-8') + message_boundary = ('--' + self.payload.get_boundary()).\ + encode('utf-8') for part in self.payload.walk(): if part.get_content_type() == "application/pkcs7-signature": signature = part.get_payload(decode=True) @@ -591,8 +588,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, self.digest_alg = verify_message( mic_content, signature, verify_cert) except IntegrityError: - mic_content = raw_content.split(message_boundary)[ - 1].replace(b'\n', b'\r\n') + mic_content = raw_content.split(message_boundary)[1].\ + replace(b'\n', b'\r\n') self.digest_alg = verify_message( mic_content, signature, verify_cert) @@ -603,15 +600,17 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, # Check for compressed data here if not self.compressed: - self.compressed, self.payload = self.decompress_data(self.payload) + self.compressed, self.payload = self._decompress_data(self.payload) except Exception as e: status = getattr(e, 'disposition_type', 'processed/Error') detailed_status = getattr( e, 'disposition_modifier', 'unexpected-processing-error') exception = (e, traceback.format_exc()) - logger.error('Failed to parse AS2 message\n: %s' % traceback.format_exc()) + logger.error('Failed to parse AS2 message\n: %s' % + traceback.format_exc()) finally: + # Update the payload headers with the original headers for k, v in as2_headers.items(): if self.payload.get(k) and k.lower() != 'content-disposition': @@ -627,13 +626,12 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, digest_alg = as2_headers.get('disposition-notification-options') if digest_alg: - digest_alg = digest_alg.split(';')[-1].split(',')[ - -1].strip() + digest_alg = digest_alg.split(';')[-1].\ + split(',')[-1].strip() mdn = Mdn( mdn_mode=mdn_mode, mdn_url=mdn_url, digest_alg=digest_alg) - mdn.build(message=self, - status=status, - detailed_status=detailed_status) + mdn.build( + message=self, status=status, detailed_status=detailed_status) return status, exception, mdn @@ -867,11 +865,12 @@ def parse(self, raw_content, find_message_cb): # orig_message.message_id, part.as_string())) mdn = part.get_payload()[-1] - mdn_status = mdn['Disposition'].split( - ';').pop().strip().split(':') + mdn_status = mdn['Disposition'].split(';').\ + pop().strip().split(':') status = mdn_status[0] if status == 'processed': - mdn_mic = mdn.get('Received-Content-MIC', '').split(',')[0] + mdn_mic = mdn.get('Received-Content-MIC', '').\ + split(',')[0] # TODO: Check MIC for all cases if mdn_mic and orig_message.mic \ @@ -910,6 +909,6 @@ def detect_mdn(self): if part.get_content_type() == 'message/disposition-notification': mdn = part.get_payload()[0] message_id = mdn.get('Original-Message-ID').strip('<>') - message_recipient = mdn.get( - 'Original-Recipient').split(';')[1].strip() + message_recipient = mdn.get('Original-Recipient').\ + split(';')[1].strip() return message_id, message_recipient diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 080e594..7e092aa 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -85,13 +85,10 @@ def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): enc_alg_list = enc_alg.split('_') cipher, key_length, mode = enc_alg_list[0], enc_alg_list[1], enc_alg_list[2] - enc_alg_asn1, key, encrypted_content = None, None, None + algorithm_id, iv, encrypted_content = None, None, None # Generate the symmetric encryption key and encrypt the message key = util.rand_bytes(int(key_length) // 8) - algorithm_id = None - iv, encrypted_content = None, None - if cipher == 'tripledes': algorithm_id = '1.2.840.113549.3.7' iv, encrypted_content = symmetric.tripledes_cbc_pkcs5_encrypt( @@ -228,12 +225,12 @@ def sign_message(data_to_sign, digest_alg, sign_key, :return: A CMS ASN.1 byte string of the signed data. """ - if use_signed_attributes: digest_func = hashlib.new(digest_alg) digest_func.update(data_to_sign) message_digest = digest_func.digest() - + print(data_to_sign) + print(digest_alg, message_digest) class SmimeCapability(core.Sequence): _fields = [ ('0', core.Any, {'optional': True}), @@ -358,7 +355,6 @@ def verify_message(data_to_verify, signature, verify_cert): cms_content = cms.ContentInfo.load(signature) digest_alg = None - if cms_content['content_type'].native == 'signed_data': for signer in cms_content['content']['signer_infos']: @@ -382,9 +378,10 @@ def verify_message(data_to_verify, signature, verify_cert): message_digest += d digest_func = hashlib.new(digest_alg) + print(data_to_verify) digest_func.update(data_to_verify) calc_message_digest = digest_func.digest() - + print(digest_alg, calc_message_digest) if message_digest != calc_message_digest: raise IntegrityError('Failed to verify message signature: ' 'Message Digest does not match.') diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index 0de6632..061d67e 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -33,6 +33,19 @@ def quote_as2name(unquoted_name): return unquoted_name +class BinaryBytesGenerator(BytesGenerator): + """ Override the bytes generator to better handle binary data """ + + def _handle_application_pkcs7_mime(self, msg): + """ Handle writing the binary messages to prevent default behaviour of + newline replacements """ + payload = msg.get_payload(decode=True) + if payload is None: + return + else: + self._fp.write(payload) + + def mime_to_bytes(msg, header_len): """ Function to convert and email Message to flat string format @@ -41,7 +54,7 @@ def mime_to_bytes(msg, header_len): :return: the byte string representation of the email message """ fp = BytesIO() - g = BytesGenerator(fp, maxheaderlen=header_len) + g = BinaryBytesGenerator(fp, maxheaderlen=header_len) g.flatten(msg) return fp.getvalue() @@ -54,18 +67,16 @@ def canonicalize(message): :return: the standard representation of the email message in bytes """ - if message.is_multipart() \ - or message.get('Content-Transfer-Encoding') != 'binary': - - return mime_to_bytes(message, 0).replace( - b'\r\n', b'\n').replace(b'\r', b'\n').replace(b'\n', b'\r\n') - else: + if message.get('Content-Transfer-Encoding') == 'binary': message_header = '' message_body = message.get_payload(decode=True) for k, v in message.items(): message_header += '{}: {}\r\n'.format(k, v) message_header += '\r\n' return message_header.encode('utf-8') + message_body + else: + return mime_to_bytes(message, 0).replace( + b'\r\n', b'\n').replace(b'\r', b'\n').replace(b'\n', b'\r\n') def make_mime_boundary(text=None): diff --git a/tests/test_basic.py b/tests/test_basic.py index 578cee8..5eadb53 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -29,13 +29,14 @@ def test_plain_message(self): # Parse the generated AS2 message as the partner in_message = as2.Message() - in_message.parse( + status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare contents of the input and output messages + self.assertEqual(status, 'processed') self.assertEqual(self.test_data, in_message.content) def test_compressed_message(self): @@ -49,15 +50,16 @@ def test_compressed_message(self): # Parse the generated AS2 message as the partner in_message = as2.Message() - in_message.parse( + status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare the mic contents of the input and output messages - self.assertEqual( - self.test_data.replace(b'\n', b'\r\n'), in_message.content) + self.assertEqual(status, 'processed') + self.assertTrue(in_message.compressed) + self.assertEqual(self.test_data, in_message.content) def test_encrypted_message(self): """ Test Encrypted Unsigned Uncompressed Message """ @@ -70,15 +72,16 @@ def test_encrypted_message(self): # Parse the generated AS2 message as the partner in_message = as2.Message() - in_message.parse( + status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare the mic contents of the input and output messages - self.assertEqual( - self.test_data.replace(b'\n', b'\r\n'), in_message.content) + self.assertEqual(status, 'processed') + self.assertTrue(in_message.encrypted) + self.assertEqual(self.test_data, in_message.content) def test_signed_message(self): """ Test Unencrypted Signed Uncompressed Message """ @@ -91,15 +94,15 @@ def test_signed_message(self): # Parse the generated AS2 message as the partner in_message = as2.Message() - in_message.parse( + status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare the mic contents of the input and output messages - self.assertEqual( - self.test_data.replace(b'\n', b'\r\n'), in_message.content) + self.assertEqual(status, 'processed') + self.assertEqual(self.test_data, in_message.content) self.assertTrue(in_message.signed) self.assertEqual(out_message.mic, in_message.mic) @@ -115,15 +118,15 @@ def test_encrypted_signed_message(self): # Parse the generated AS2 message as the partner in_message = as2.Message() - in_message.parse( + status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare the mic contents of the input and output messages - self.assertEqual( - self.test_data.replace(b'\n', b'\r\n'), in_message.content) + self.assertEqual(status, 'processed') + self.assertEqual(self.test_data, in_message.content) self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertEqual(out_message.mic, in_message.mic) @@ -149,8 +152,7 @@ def test_encrypted_signed_compressed_message(self): # Compare the mic contents of the input and output messages self.assertEqual(status, 'processed') - self.assertEqual( - self.test_data.replace(b'\n', b'\r\n'), in_message.content) + self.assertEqual(self.test_data, in_message.content) self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertTrue(in_message.compressed) From 21e0f218818578c4d7d73d47dd4ffefaad7a29cf Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 30 Apr 2019 15:52:08 +0530 Subject: [PATCH 15/66] look for all signature types --- CHANGELOG.md | 1 + pyas2lib/as2.py | 52 +++++++++++++++++++++++++------------------------ 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 056a904..57a110d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Handle cases where compression is done before signing. * Add support for additional encryption algorithms. * Use binary encoding for encryption and signatures. +* Look for `application/x-pkcs7-signature` when verifying signatures. * Remove support for Python 2. ## 1.0.3 - 2018-05-01 diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 3e5a71a..4d06edf 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -542,11 +542,11 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, if self.payload.get_content_type() == 'application/pkcs7-mime' \ and self.payload.get_param('smime-type') == 'enveloped-data': - encrypted_data = self.payload.get_payload(decode=True) - # logger.debug( - # 'Decrypting the payload :\n%s' % self.payload.as_string()) + logger.debug('Decrypting message %s payload :\n%s' % ( + self.message_id, self.payload.as_string())) self.encrypted = True + encrypted_data = self.payload.get_payload(decode=True) self.enc_alg, decrypted_content = decrypt_message( encrypted_data, self.receiver.decrypt_key) @@ -568,14 +568,16 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, 'but signed message not found.'.format(partner_id)) if self.payload.get_content_type() == 'multipart/signed': - # logger.debug(b'Verifying the signed payload:\n{0:s}'.format( - # self.payload.as_string())) + logger.debug('Verifying signed message %s payload: \n%s' % ( + self.message_id, self.payload.as_string())) self.signed = True + + # Split the message into signature and signed message signature = None - message_boundary = ('--' + self.payload.get_boundary()).\ - encode('utf-8') + signature_types = ['application/pkcs7-signature', + 'application/x-pkcs7-signature'] for part in self.payload.walk(): - if part.get_content_type() == "application/pkcs7-signature": + if part.get_content_type() in signature_types: signature = part.get_payload(decode=True) else: self.payload = part @@ -584,14 +586,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, # then convert to canonical form and try again mic_content = canonicalize(self.payload) verify_cert = self.sender.load_verify_cert() - try: - self.digest_alg = verify_message( - mic_content, signature, verify_cert) - except IntegrityError: - mic_content = raw_content.split(message_boundary)[1].\ - replace(b'\n', b'\r\n') - self.digest_alg = verify_message( - mic_content, signature, verify_cert) + self.digest_alg = verify_message( + mic_content, signature, verify_cert) # Calculate the MIC Hash of the message to be verified digest_func = hashlib.new(self.digest_alg) @@ -753,8 +749,8 @@ def build(self, message, status, detailed_status=None): encoders.encode_7or8bit(mdn_base) self.payload.attach(mdn_base) - # logger.debug('MDN for message %s created:\n%s' % ( - # message.message_id, mdn_base.as_string())) + logger.debug('MDN for message %s created:\n%s' % ( + message.message_id, mdn_base.as_string())) # Sign the MDN if it is requested by the sender if message.headers.get('disposition-notification-options') and \ @@ -775,18 +771,20 @@ def build(self, message, status, detailed_status=None): signature.add_header( 'Content-Disposition', 'attachment', filename='smime.p7s') del signature['MIME-Version'] + signature.set_payload(sign_message( canonicalize(self.payload), self.digest_alg, message.receiver.sign_key )) encoders.encode_base64(signature) - # logger.debug( - # 'Signature for MDN created:\n%s' % signature.as_string()) + signed_mdn.set_param('micalg', self.digest_alg) signed_mdn.attach(signature) self.payload = signed_mdn + logger.debug('Signature for MDN %s created:\n%s' % ( + message.message_id, signature.as_string())) # Update the headers of the final payload and set message boundary for k, v in mdn_headers.items(): @@ -838,11 +836,15 @@ def parse(self, raw_content, find_message_cb): return status, detailed_status if self.payload.get_content_type() == 'multipart/signed': + message_boundary = ('--' + self.payload.get_boundary()).\ + encode('utf-8') + + # Extract the signature and the signed payload signature = None - message_boundary = ( - '--' + self.payload.get_boundary()).encode('utf-8') + signature_types = ['application/pkcs7-signature', + 'application/x-pkcs7-signature'] for part in self.payload.walk(): - if part.get_content_type() == 'application/pkcs7-signature': + if part.get_content_type() in signature_types: signature = part.get_payload(decode=True) elif part.get_content_type() == 'multipart/report': self.payload = part @@ -861,8 +863,8 @@ def parse(self, raw_content, find_message_cb): for part in self.payload.walk(): if part.get_content_type() == 'message/disposition-notification': - # logger.debug('Found MDN report for message %s:\n%s' % ( - # orig_message.message_id, part.as_string())) + logger.debug('Found MDN report for message %s:\n%s' % ( + orig_message.message_id, part.as_string())) mdn = part.get_payload()[-1] mdn_status = mdn['Disposition'].split(';').\ From 43c7b0fe2e544b887ba874ea09af4dc7d489c5c8 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 30 Apr 2019 15:58:01 +0530 Subject: [PATCH 16/66] Bump version --- .travis.yml | 1 + CHANGELOG.md | 2 +- pyas2lib/__init__.py | 38 +++++++++++++++++--------------------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3349ac0..d5b2585 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: xenial language: python python: - '3.5' diff --git a/CHANGELOG.md b/CHANGELOG.md index 57a110d..11121cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 1.0.4 - 2019-04-26 +## 1.1.0 - 2019-04-30 * Handle cases where compression is done before signing. * Add support for additional encryption algorithms. diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 2b8876b..7ed9010 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -1,28 +1,24 @@ -from __future__ import absolute_import -# from pyas2lib.as2 import DIGEST_ALGORITHMS, ENCRYPTION_ALGORITHMS,\ -# MDN_CONFIRM_TEXT, MDN_FAILED_TEXT, Partner, Organization, Message, Mdn -import sys +from pyas2lib.as2 import DIGEST_ALGORITHMS +from pyas2lib.as2 import ENCRYPTION_ALGORITHMS +from pyas2lib.as2 import MDN_CONFIRM_TEXT +from pyas2lib.as2 import MDN_FAILED_TEXT +from pyas2lib.as2 import Mdn +from pyas2lib.as2 import Message +from pyas2lib.as2 import Organization +from pyas2lib.as2 import Partner -VERSION = (1, 0, 3) +VERSION = (1, 1, 0) __version__ = '.'.join(map(str, VERSION)) __all__ = [ 'VERSION', - # 'DIGEST_ALGORITHMS', - # 'ENCRYPTION_ALGORITHMS', - # 'MDN_CONFIRM_TEXT', - # 'MDN_FAILED_TEXT', - # 'Partner', - # 'Organization', - # 'Message', - # 'Mdn' + 'DIGEST_ALGORITHMS', + 'ENCRYPTION_ALGORITHMS', + 'MDN_CONFIRM_TEXT', + 'MDN_FAILED_TEXT', + 'Partner', + 'Organization', + 'Message', + 'Mdn' ] - -if (2, 7) <= sys.version_info < (3, 2): - # On Python 2.7 and Python3 < 3.2, install no-op handler to silence - # `No handlers could be found for logger "elasticsearch"` message per - # - import logging - logger = logging.getLogger('pyas2lib') - logger.addHandler(logging.NullHandler()) From e7ff37f86f9593da170939172f6776f3ec631bf4 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 30 Apr 2019 16:04:39 +0530 Subject: [PATCH 17/66] Bump version --- pyas2lib/__init__.py | 4 +--- setup.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 7ed9010..7238017 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -7,12 +7,10 @@ from pyas2lib.as2 import Organization from pyas2lib.as2 import Partner -VERSION = (1, 1, 0) -__version__ = '.'.join(map(str, VERSION)) +__version__ = '1.1.0' __all__ = [ - 'VERSION', 'DIGEST_ALGORITHMS', 'ENCRYPTION_ALGORITHMS', 'MDN_CONFIRM_TEXT', diff --git a/setup.py b/setup.py index 8c5502a..6bcc851 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ from setuptools import setup, find_packages -version = __import__('pyas2lib').__version__ - install_requires = [ 'asn1crypto==0.24.0', 'oscrypto==0.19.1', @@ -24,7 +22,7 @@ long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version=version, + version='1.1.0', author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages( From f239930c9f258577318396cee4012e5e4b290ed1 Mon Sep 17 00:00:00 2001 From: Wassilios Lytras Date: Mon, 3 Jun 2019 14:03:55 +0200 Subject: [PATCH 18/66] Extract certificate information to dictionary function. --- pyas2lib/utils.py | 48 +++++++++++++++++++++++ tests/__init__.py | 18 ++++++++- tests/fixtures/cert_extract_private.cer | Bin 0 -> 776 bytes tests/fixtures/cert_extract_private.pem | 49 ++++++++++++++++++++++++ tests/fixtures/cert_extract_public.cer | Bin 0 -> 776 bytes tests/fixtures/cert_extract_public.pem | 19 +++++++++ tests/test_advanced.py | 26 ++++++++++++- 7 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/cert_extract_private.cer create mode 100644 tests/fixtures/cert_extract_private.pem create mode 100644 tests/fixtures/cert_extract_public.cer create mode 100644 tests/fixtures/cert_extract_public.pem 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 0000000000000000000000000000000000000000..d984cce2382ee69176f397f91fa71805c26cd71d GIT binary patch literal 776 zcmXqLVrDUDVtm8I$*}MElASRYjXw=|**LY@JlekVGBR?rG8niRavN~6F^96S2{So{ z8VVZ-f;b#JT*3Ldsk*@>i6yCqf(HB`K`tJSg382VqnyknLtX=JkPtHuOG#>RiGiFr zuc4)ZnSrsPp|O#vd6YP>k)?qtkO$#X$(ANYC1mF_vNA9?G4eA2eaOYs#K_37zjLME z6y`S}S$pS~v$O;rw7$5PUoCaw>$1j3(aE2C#jn3=6ffP8p53;p@34D8Z)^KM@pT$; zta%>26871jlv{7eUz=yIBrti4;kj&$Ma#OUZ#(?o!uV=vlBDM13v65WYTZ#@tF-Lp zv!&LeR-3F&U*4m(^&W$0nBcQdW!*r>x@5HMz z+RP3S+Y$YiclpmtmoIO5w(zu{QGVke8>^gq|E3g+J?{T|T$m~J{-aqhB07&+ua}=4 zeC?Xem7`3|j0}v(Aq0$IUr>f#8=#X5a&9NlR5u?hH!EDwni0eme8$nYFZMV*;CW6ENoq2bMr&BmE@vX zM;J4+kG`^TG5q^H^rYnUo4P(?bF+lH-%Y*pCg~E>y}vVOa~ZNNzW=$u`r+|3@fBah zc3%GbtT8a5xy|x_OqBQ2nJ;+tJiYmng#%XVZVv43+EuG;7b^UEebf@kW7EqXT``}} zct1qx?1@9dy3>2OuX-_PedyxQyZb+i6yCqf(HB`K`tJSg382VqnyknLtX=JkPtHuOG#>RiGiFr zuc4)ZnSrsPp|O#vd6YP>k)?qtkO$#X$(ANYC1mF_vNA9?G4eA2eaOYs#K_37zjLME z6y`S}S$pS~v$O;rw7$5PUoCaw>$1j3(aE2C#jn3=6ffP8p53;p@34D8Z)^KM@pT$; zta%>26871jlv{7eUz=yIBrti4;kj&$Ma#OUZ#(?o!uV=vlBDM13v65WYTZ#@tF-Lp zv!&LeR-3F&U*4m(^&W$0nBcQdW!*r>x@5HMz z+RP3S+Y$YiclpmtmoIO5w(zu{QGVke8>^gq|E3g+J?{T|T$m~J{-aqhB07&+ua}=4 zeC?Xem7`3|j0}v(Aq0$IUr>f#8=#X5a&9NlR5u?hH!EDwni0eme8$nYFZMV*;CW6ENoq2bMr&BmE@vX zM;J4+kG`^TG5q^H^rYnUo4P(?bF+lH-%Y*pCg~E>y}vVOa~ZNNzW=$u`r+|3@fBah zc3%GbtT8a5xy|x_OqBQ2nJ;+tJiYmng#%XVZVv43+EuG;7b^UEebf@kW7EqXT``}} zct1qx?1@9dy3>2OuX-_PedyxQyZb+ Date: Mon, 3 Jun 2019 20:24:50 +0530 Subject: [PATCH 19/66] remove print statements --- .gitignore | 3 ++- pyas2lib/cms.py | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 70eeea2..3ecab8f 100644 --- a/.gitignore +++ b/.gitignore @@ -91,4 +91,5 @@ ENV/ # IDEA .idea .pytest_cache/ -.coverage \ No newline at end of file +.coverage +.DS_Store \ No newline at end of file diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 7e092aa..b40df8b 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -229,8 +229,7 @@ def sign_message(data_to_sign, digest_alg, sign_key, digest_func = hashlib.new(digest_alg) digest_func.update(data_to_sign) message_digest = digest_func.digest() - print(data_to_sign) - print(digest_alg, message_digest) + class SmimeCapability(core.Sequence): _fields = [ ('0', core.Any, {'optional': True}), @@ -378,10 +377,8 @@ def verify_message(data_to_verify, signature, verify_cert): message_digest += d digest_func = hashlib.new(digest_alg) - print(data_to_verify) digest_func.update(data_to_verify) calc_message_digest = digest_func.digest() - print(digest_alg, calc_message_digest) if message_digest != calc_message_digest: raise IntegrityError('Failed to verify message signature: ' 'Message Digest does not match.') From afa25f30857037c0897d03de03c08e4451f2228e Mon Sep 17 00:00:00 2001 From: abhishekram Date: Mon, 3 Jun 2019 20:30:46 +0530 Subject: [PATCH 20/66] some styling fixes --- pyas2lib/utils.py | 23 ++++++++++++------- tests/test_advanced.py | 52 ++++++++++++++++++++++++------------------ 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index 40e985e..d289795 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -191,7 +191,9 @@ def verify_certificate_chain(cert_str, trusted_certs, ignore_self_signed=True): def extract_certificate_info(cert): """ Extract validity information from the certificate and return a dictionary. - Provide either key with certificate (private) or public certificate + + 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) @@ -216,16 +218,21 @@ def extract_certificate_info(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 + # 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['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: diff --git a/tests/test_advanced.py b/tests/test_advanced.py index ce39ce3..06fd12f 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -4,6 +4,7 @@ import base64 import datetime + class TestAdvanced(Pyas2TestCase): def setUp(self): @@ -72,7 +73,7 @@ def test_partner_not_found(self): _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_none, + find_partner_cb=lambda x: None, find_message_cb=lambda x, y: False ) @@ -88,7 +89,7 @@ def test_partner_not_found(self): in_message = as2.Message() _, _, mdn = in_message.parse( raw_out_message, - find_org_cb=self.find_none, + find_org_cb=lambda x: None, find_partner_cb=self.find_partner, find_message_cb=lambda x, y: False ) @@ -326,36 +327,43 @@ def test_load_private_key(self): 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} + """ 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(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 def find_partner(self, headers): return self.partner - def find_none(self, as2_id): - return None - def find_message(self, message_id, message_recipient): return self.out_message From bca5d62bc16b948054fbe093e7370a569996b4b2 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Mon, 3 Jun 2019 20:35:09 +0530 Subject: [PATCH 21/66] bump version to 1.1.1 --- AUTHORS.md | 3 ++- CHANGELOG.md | 5 +++++ pyas2lib/__init__.py | 2 +- setup.py | 3 +-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 031b8bf..ce5df28 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1 +1,2 @@ -* Abhishek Ram @abhishek-ram \ No newline at end of file +* Abhishek Ram @abhishek-ram +* Chad Gates @chadgates \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 11121cd..9781838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 1.1.1 - 2019-06-03 + +* Remove leftover print statement. +* Add utility for extracting public certificate information. + ## 1.1.0 - 2019-04-30 * Handle cases where compression is done before signing. diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 7238017..eefd495 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -7,7 +7,7 @@ from pyas2lib.as2 import Organization from pyas2lib.as2 import Partner -__version__ = '1.1.0' +__version__ = '1.1.1' __all__ = [ diff --git a/setup.py b/setup.py index 6bcc851..b7efad9 100644 --- a/setup.py +++ b/setup.py @@ -18,11 +18,10 @@ description="Python library for building and parsing AS2 Messages", license="GNU GPL v2.0", url="https://github.com/abhishek-ram/pyas2-lib", - # long_description=long_description, long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version='1.1.0', + version='1.1.1', author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages( From 52258be0f23a50947dc0e8b8aca97da98da7a5fa Mon Sep 17 00:00:00 2001 From: Wassilios Lytras Date: Tue, 4 Jun 2019 14:04:50 +0200 Subject: [PATCH 22/66] Prevent RuntimeWarning: DateTimeField received a naive datetime in django-pyas2 --- pyas2lib/utils.py | 4 ++-- tests/test_advanced.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index d289795..f03d111 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -224,9 +224,9 @@ def extract_certificate_info(cert): # 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") + certificate.get_notBefore().decode('utf8'), "%Y%m%d%H%M%S%z") cert_info['valid_to'] = datetime.strptime( - certificate.get_notAfter().decode('utf8'), "%Y%m%d%H%M%SZ") + certificate.get_notAfter().decode('utf8'), "%Y%m%d%H%M%S%z") cert_info['subject'] = [ tuple(item.decode('utf8') for item in sets) for sets in certificate.get_subject().get_components()] diff --git a/tests/test_advanced.py b/tests/test_advanced.py index 06fd12f..f5fd1ac 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -331,8 +331,8 @@ def test_extract_certificate_info(self): 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), + 'valid_from': datetime.datetime(2019, 6, 3, 11, 32, 57, tzinfo=datetime.timezone.utc), + 'valid_to': datetime.datetime(2029, 5, 31, 11, 32, 57, tzinfo=datetime.timezone.utc), 'subject': [('C', 'AU'), ('ST', 'Some-State'), ('O', 'pyas2lib'), ('CN', 'test')], 'issuer': [('C', 'AU'), ('ST', 'Some-State'), From 6cd2cb09ac59d347304c3bd746dc0d2245f566fd Mon Sep 17 00:00:00 2001 From: Wassilios Lytras Date: Tue, 4 Jun 2019 16:07:26 +0200 Subject: [PATCH 23/66] Prevent RuntimeWarning: DateTimeField received a naive datetime in django-pyas2 --- pyas2lib/utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index f03d111..22eca06 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -8,7 +8,7 @@ from io import BytesIO from pyas2lib.exceptions import AS2Exception -from datetime import datetime +from datetime import datetime, timezone def unquote_as2name(quoted_name): @@ -196,8 +196,8 @@ def extract_certificate_info(cert): :param cert: the certificate as byte string in PEM or DER format :return: a dictionary holding certificate information: - valid_from (datetime) - valid_to (datetime) + valid_from (datetime) - UTC + valid_to (datetime) - UTC subject (list of name, value tuples) issuer (list of name, value tuples) serial (int) @@ -224,9 +224,11 @@ def extract_certificate_info(cert): # 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%S%z") + certificate.get_notBefore().decode('utf8'), "%Y%m%d%H%M%SZ").\ + replace(tzinfo=timezone.utc) cert_info['valid_to'] = datetime.strptime( - certificate.get_notAfter().decode('utf8'), "%Y%m%d%H%M%S%z") + certificate.get_notAfter().decode('utf8'), "%Y%m%d%H%M%SZ").\ + replace(tzinfo=timezone.utc) cert_info['subject'] = [ tuple(item.decode('utf8') for item in sets) for sets in certificate.get_subject().get_components()] From bb365f3d4d588af7351f56c458ebdce9cbd71637 Mon Sep 17 00:00:00 2001 From: Wassilios Lytras Date: Fri, 7 Jun 2019 21:15:28 +0200 Subject: [PATCH 24/66] Adding option to pass/define disposition notification to when creating AS2 message. --- pyas2lib/as2.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 4d06edf..7e946c9 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -305,7 +305,8 @@ def headers_str(self): return message_header.encode('utf-8') def build(self, data, filename=None, subject='AS2 Message', - content_type='application/edi-consent', additional_headers=None): + content_type='application/edi-consent', additional_headers=None, + disposition_notification_to='no-reply@pyas2.com'): """Function builds the AS2 message. Compresses, signs and encrypts the payload if applicable. @@ -326,6 +327,10 @@ def build(self, data, filename=None, subject='AS2 Message', :param additional_headers: Any additional headers to be included as part of the AS2 message. + :param disposition_notification_to: + Email address for disposition-notification-to header entry. + (default "no-reply@pyas2.com") + """ # Validations @@ -441,7 +446,7 @@ def build(self, data, filename=None, subject='AS2 Message', self.message_id, self.payload.as_string())) if self.receiver.mdn_mode: - as2_headers['disposition-notification-to'] = 'no-reply@pyas2.com' + as2_headers['disposition-notification-to'] = disposition_notification_to if self.receiver.mdn_digest_alg: as2_headers['disposition-notification-options'] = \ 'signed-receipt-protocol=required, pkcs7-signature; ' \ From f21ee0ed01364f87fc4fe978e1aac814430cfa3c Mon Sep 17 00:00:00 2001 From: Wassilios Lytras Date: Mon, 10 Jun 2019 15:31:28 +0200 Subject: [PATCH 25/66] Remove unused variable. --- pyas2lib/as2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 7e946c9..23c3dbc 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -555,7 +555,6 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, self.enc_alg, decrypted_content = decrypt_message( encrypted_data, self.receiver.decrypt_key) - raw_content = decrypted_content self.payload = parse_mime(decrypted_content) if self.payload.get_content_type() == 'text/plain': From 946d598584aa446833d2ce8e68bbde10611a4747 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 11 Jun 2019 09:24:47 +0530 Subject: [PATCH 26/66] use dataclasses for organization and partner --- .travis.yml | 1 - pyas2lib/as2.py | 196 ++++++++++++++++++++------------------ setup.py | 7 +- tests/test_advanced.py | 13 +-- tests/test_basic.py | 6 +- tests/test_mdn.py | 4 +- tests/test_with_mecas2.py | 4 +- 7 files changed, 122 insertions(+), 109 deletions(-) diff --git a/.travis.yml b/.travis.yml index d5b2585..4e8d6f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ dist: xenial language: python python: - - '3.5' - '3.6' - '3.7' install: diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 4d06edf..17596c1 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -2,6 +2,7 @@ import hashlib import binascii import traceback +from dataclasses import dataclass from email import encoders from email import message as email_message from email import message_from_bytes as parse_mime @@ -51,42 +52,49 @@ 'disposition-notification report has additional details.' +@dataclass class Organization(object): - """Class represents an AS2 organization and defines the certificates and - settings to be used when sending and receiving messages. """ + """ + Class represents an AS2 organization and defines the certificates and + settings to be used when sending and receiving messages. - def __init__(self, as2_name, sign_key=None, sign_key_pass=None, - decrypt_key=None, decrypt_key_pass=None, mdn_url=None, - mdn_confirm_text=MDN_CONFIRM_TEXT): - """ - :param as2_name: The unique AS2 name for this organization + :param as2_name: The unique AS2 name for this organization - :param sign_key: A byte string of the pkcs12 encoded key pair - used for signing outbound messages and MDNs. + :param sign_key: A byte string of the pkcs12 encoded key pair + used for signing outbound messages and MDNs. - :param sign_key_pass: The password for decrypting the `sign_key` + :param sign_key_pass: The password for decrypting the `sign_key` - :param decrypt_key: A byte string of the pkcs12 encoded key pair - used for decrypting inbound messages. + :param decrypt_key: A byte string of the pkcs12 encoded key pair + used for decrypting inbound messages. - :param decrypt_key_pass: The password for decrypting the `decrypt_key` + :param decrypt_key_pass: The password for decrypting the `decrypt_key` - :param mdn_url: The URL where the receiver is expected to post - asynchronous MDNs. - """ - self.sign_key = self.load_key( - sign_key, sign_key_pass) if sign_key else None + :param mdn_url: The URL where the receiver is expected to post + asynchronous MDNs. + """ - self.decrypt_key = self.load_key( - decrypt_key, decrypt_key_pass) if decrypt_key else None + as2_name: str + sign_key: bytes = None + sign_key_pass: str = None + decrypt_key: bytes = None + decrypt_key_pass: str = None + mdn_url: str = None + mdn_confirm_text: str = MDN_CONFIRM_TEXT - self.as2_name = as2_name - self.mdn_url = mdn_url - self.mdn_confirm_text = mdn_confirm_text + def __post_init__(self): + """Run the post initialisation checks for this class.""" + # Load the signature and decryption keys + if self.sign_key: + self.sign_key = self.load_key(self.sign_key, self.sign_key_pass) + + if self.decrypt_key: + self.decrypt_key = self.load_key( + self.decrypt_key, self.decrypt_key_pass) @staticmethod - def load_key(key_str, key_pass): - """ Function to load password protected key file in p12 or pem format""" + def load_key(key_str: bytes, key_pass: str): + """Function to load password protected key file in p12 or pem format.""" try: # First try to parse as a p12 file @@ -115,99 +123,99 @@ def load_key(key_str, key_pass): return key, cert +@dataclass class Partner(object): - """Class represents an AS2 partner and defines the certificates and - settings to be used when sending and receiving messages.""" - - def __init__(self, as2_name, verify_cert=None, verify_cert_ca=None, - encrypt_cert=None, encrypt_cert_ca=None, validate_certs=True, - compress=False, sign=False, digest_alg='sha256', encrypt=False, - enc_alg='tripledes_192_cbc', mdn_mode=None, - mdn_digest_alg=None, mdn_confirm_text=MDN_CONFIRM_TEXT): - """ - :param as2_name: The unique AS2 name for this partner. + """ + Class represents an AS2 partner and defines the certificates and + settings to be used when sending and receiving messages. - :param verify_cert: A byte string of the certificate to be used for - verifying signatures of inbound messages and MDNs. + :param as2_name: The unique AS2 name for this partner. - :param verify_cert_ca: A byte string of the ca certificate if any of - the verification cert + :param verify_cert: A byte string of the certificate to be used for + verifying signatures of inbound messages and MDNs. - :param encrypt_cert: A byte string of the certificate to be used for - encrypting outbound message. + :param verify_cert_ca: A byte string of the ca certificate if any of + the verification cert - :param encrypt_cert_ca: A byte string of the ca certificate if any of - the encryption cert + :param encrypt_cert: A byte string of the certificate to be used for + encrypting outbound message. - :param validate_certs: Set this flag to `False` to disable validations of - the encryption and verification certificates. (default `True`) + :param encrypt_cert_ca: A byte string of the ca certificate if any of + the encryption cert - :param compress: Set this flag to `True` to compress outgoing - messages. (default `False`) + :param validate_certs: Set this flag to `False` to disable validations of + the encryption and verification certificates. (default `True`) - :param sign: Set this flag to `True` to sign outgoing - messages. (default `False`) + :param compress: Set this flag to `True` to compress outgoing + messages. (default `False`) - :param digest_alg: The digest algorithm to be used for generating the - signature. (default "sha256") + :param sign: Set this flag to `True` to sign outgoing + messages. (default `False`) - :param encrypt: Set this flag to `True` to encrypt outgoing - messages. (default `False`) + :param digest_alg: The digest algorithm to be used for generating the + signature. (default "sha256") - :param enc_alg: - The encryption algorithm to be used. (default `"tripledes_192_cbc"`) + :param encrypt: Set this flag to `True` to encrypt outgoing + messages. (default `False`) - :param mdn_mode: The mode to be used for receiving the MDN. - Set to `None` for no MDN, `'SYNC'` for synchronous and `'ASYNC'` - for asynchronous. (default `None`) + :param enc_alg: + The encryption algorithm to be used. (default `"tripledes_192_cbc"`) - :param mdn_digest_alg: The digest algorithm to be used by the receiver - for signing the MDN. Use `None` for unsigned MDN. (default `None`) + :param cms_encoding: + The encoding to be used for the encrypted, signed or compressed data. + It can be `'binary'` or `'base64'`. (default `'binary'`) - :param mdn_confirm_text: The text to be used in the MDN for successfully - processed messages received from this partner. + :param mdn_mode: The mode to be used for receiving the MDN. + Set to `None` for no MDN, `'SYNC'` for synchronous and `'ASYNC'` + for asynchronous. (default `None`) - """ + :param mdn_digest_alg: The digest algorithm to be used by the receiver + for signing the MDN. Use `None` for unsigned MDN. (default `None`) + + :param mdn_confirm_text: The text to be used in the MDN for successfully + processed messages received from this partner. + + """ + + as2_name: str + verify_cert: bytes = None + verify_cert_ca: bytes = None + encrypt_cert: bytes = None + encrypt_cert_ca: bytes = None + validate_certs: bool = True + compress: bool = False + encrypt: bool = False + enc_alg: str = 'tripledes_192_cbc' + sign: bool = False + digest_alg: str = 'sha256' + cms_encoding: str = 'binary' + mdn_mode: str = None + mdn_digest_alg: str = None + mdn_confirm_text: str = MDN_CONFIRM_TEXT + + def __post_init__(self): + """Run the post initialisation checks for this class.""" # Validations - if digest_alg and digest_alg not in DIGEST_ALGORITHMS: + if self.digest_alg and self.digest_alg not in DIGEST_ALGORITHMS: raise ImproperlyConfigured( - 'Unsupported Digest Algorithm {}, must be ' - 'one of {}'.format(digest_alg, DIGEST_ALGORITHMS)) + f'Unsupported Digest Algorithm {self.digest_alg}, must be ' + f'one of {DIGEST_ALGORITHMS}') - if enc_alg and enc_alg not in ENCRYPTION_ALGORITHMS: + if self.enc_alg and self.enc_alg not in ENCRYPTION_ALGORITHMS: raise ImproperlyConfigured( - 'Unsupported Encryption Algorithm {}, must be ' - 'one of {}'.format(enc_alg, ENCRYPTION_ALGORITHMS)) + f'Unsupported Encryption Algorithm {self.enc_alg}, must be ' + f'one of {ENCRYPTION_ALGORITHMS}') - if mdn_mode and mdn_mode not in MDN_MODES: + if self.mdn_mode and self.mdn_mode not in MDN_MODES: raise ImproperlyConfigured( - 'Unsupported MDN Mode {}, must be ' - 'one of {}'.format(digest_alg, MDN_MODES)) + f'Unsupported MDN Mode {self.mdn_mode}, must be ' + f'one of {MDN_MODES}') - # if mdn_mode == 'ASYNC' and not mdn_url: - # raise ImproperlyConfigured( - # 'mdn_url is mandatory when mdn_mode is set to ASYNC ') - - if mdn_digest_alg and mdn_digest_alg not in DIGEST_ALGORITHMS: + if self.mdn_digest_alg and self.mdn_digest_alg not in DIGEST_ALGORITHMS: raise ImproperlyConfigured( - 'Unsupported MDN Digest Algorithm {}, must be ' - 'one of {}'.format(mdn_digest_alg, DIGEST_ALGORITHMS)) - - self.as2_name = as2_name - self.compress = compress - self.sign = sign - self.digest_alg = digest_alg - self.encrypt = encrypt - self.enc_alg = enc_alg - self.mdn_mode = mdn_mode - self.mdn_digest_alg = mdn_digest_alg - self.mdn_confirm_text = mdn_confirm_text - self.verify_cert = verify_cert - self.verify_cert_ca = verify_cert_ca - self.encrypt_cert = encrypt_cert - self.encrypt_cert_ca = encrypt_cert_ca - self.validate_certs = validate_certs + f'Unsupported MDN Digest Algorithm {self.mdn_digest_alg}, ' + f'must be one of {DIGEST_ALGORITHMS}') def load_verify_cert(self): if self.validate_certs: diff --git a/setup.py b/setup.py index b7efad9..e58394b 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +import sys from setuptools import setup, find_packages install_requires = [ @@ -6,6 +7,11 @@ 'pyOpenSSL==17.5.0', ] +if sys.version_info.minor == 6: + install_requires += [ + 'dataclasses==0.6' + ] + tests_require = [ 'pytest==3.4.0', 'pytest-cov==2.5.1', @@ -34,7 +40,6 @@ "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Security :: Cryptography", diff --git a/tests/test_advanced.py b/tests/test_advanced.py index 06fd12f..4c021f4 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -11,9 +11,9 @@ def setUp(self): self.org = as2.Organization( as2_name='some_organization', sign_key=self.private_key, - sign_key_pass='test'.encode('utf-8'), + sign_key_pass='test', decrypt_key=self.private_key, - decrypt_key_pass='test'.encode('utf-8') + decrypt_key_pass='test' ) self.partner = as2.Partner( as2_name='some_partner', @@ -82,6 +82,7 @@ def test_partner_not_found(self): mdn.headers_str + b'\r\n' + mdn.content, find_message_cb=self.find_message ) + self.assertEqual(status, 'processed/Error') self.assertEqual(detailed_status, 'unknown-trading-partner') @@ -309,7 +310,7 @@ def test_load_private_key(self): as2.Organization( as2_name='some_org', sign_key=cert_file.read(), - sign_key_pass=b'test' + sign_key_pass='test' ) except as2.AS2Exception as e: self.fail('Failed to load p12 private key: %s' % e) @@ -321,7 +322,7 @@ def test_load_private_key(self): as2.Organization( as2_name='some_org', sign_key=cert_file.read(), - sign_key_pass=b'test' + sign_key_pass='test' ) except as2.AS2Exception as e: self.fail('Failed to load pem private key: %s' % e) @@ -374,9 +375,9 @@ def setUp(self): self.org = as2.Organization( as2_name='AS2 Server', sign_key=self.oldpyas2_private_key, - sign_key_pass='password'.encode('utf-8'), + sign_key_pass='password', decrypt_key=self.oldpyas2_private_key, - decrypt_key_pass='password'.encode('utf-8') + decrypt_key_pass='password' ) self.partner = as2.Partner( as2_name='Sterling B2B Integrator', diff --git a/tests/test_basic.py b/tests/test_basic.py index 5eadb53..1a959e5 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -8,9 +8,9 @@ def setUp(self): self.org = as2.Organization( as2_name='some_organization', sign_key=self.private_key, - sign_key_pass='test'.encode('utf-8'), + sign_key_pass='test', decrypt_key=self.private_key, - decrypt_key_pass='test'.encode('utf-8') + decrypt_key_pass='test' ) self.partner = as2.Partner( as2_name='some_partner', @@ -126,10 +126,10 @@ def test_encrypted_signed_message(self): # Compare the mic contents of the input and output messages self.assertEqual(status, 'processed') - self.assertEqual(self.test_data, in_message.content) self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertEqual(out_message.mic, in_message.mic) + self.assertEqual(self.test_data, in_message.content) def test_encrypted_signed_compressed_message(self): """ Test Encrypted Signed Compressed Message """ diff --git a/tests/test_mdn.py b/tests/test_mdn.py index 96aa5a9..5e33457 100644 --- a/tests/test_mdn.py +++ b/tests/test_mdn.py @@ -9,9 +9,9 @@ def setUp(self): self.org = as2.Organization( as2_name='some_organization', sign_key=self.private_key, - sign_key_pass='test'.encode('utf-8'), + sign_key_pass='test', decrypt_key=self.private_key, - decrypt_key_pass='test'.encode('utf-8') + decrypt_key_pass='test' ) self.partner = as2.Partner( as2_name='some_partner', diff --git a/tests/test_with_mecas2.py b/tests/test_with_mecas2.py index 3c48dd5..6a34a02 100644 --- a/tests/test_with_mecas2.py +++ b/tests/test_with_mecas2.py @@ -9,9 +9,9 @@ def setUp(self): self.org = as2.Organization( as2_name='some_organization', sign_key=self.private_key, - sign_key_pass='test'.encode('utf-8'), + sign_key_pass='test', decrypt_key=self.private_key, - decrypt_key_pass='test'.encode('utf-8') + decrypt_key_pass='test' ) self.partner = as2.Partner( as2_name='mecas2', From 1d6f97785786799e71b5aebaf846a7185844450e Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 11 Jun 2019 09:37:48 +0530 Subject: [PATCH 27/66] restructure test cases to be part of the library --- .coveragerc | 22 ++++++++++++++ .gitignore | 1 - {tests => pyas2lib/tests}/__init__.py | 4 --- .../tests}/fixtures/cert_extract_private.cer | Bin .../tests}/fixtures/cert_extract_private.pem | 0 .../tests}/fixtures/cert_extract_public.cer | Bin .../tests}/fixtures/cert_extract_public.pem | 0 .../tests}/fixtures/cert_mecas2_public.pem | 0 .../tests}/fixtures/cert_oldpyas2_private.pem | 0 .../tests}/fixtures/cert_oldpyas2_public.pem | 0 .../tests}/fixtures/cert_sb2bi_public.ca | 0 .../tests}/fixtures/cert_sb2bi_public.pem | 0 .../tests}/fixtures/cert_test.p12 | Bin .../tests}/fixtures/cert_test.pem | 0 .../tests}/fixtures/cert_test_public.pem | 0 .../tests}/fixtures/mecas2_compressed.as2 | Bin .../mecas2_compressed_signed_encrypted.as2 | Bin .../tests}/fixtures/mecas2_encrypted.as2 | Bin .../tests}/fixtures/mecas2_signed.as2 | 0 .../tests}/fixtures/mecas2_signed.mdn | 0 .../fixtures/mecas2_signed_encrypted.as2 | Bin .../tests}/fixtures/mecas2_unsigned.mdn | 0 .../tests}/fixtures/payload.binary | Bin .../tests}/fixtures/payload.txt | 0 pyas2lib/tests/fixtures/payload_dos.txt | 28 ++++++++++++++++++ .../tests}/fixtures/sb2bi_signed.mdn | Bin .../tests}/fixtures/sb2bi_signed_cmp.msg | Bin .../tests}/fixtures/verify_cert_test1.pem | 0 .../tests}/fixtures/verify_cert_test2.cer | Bin .../tests}/fixtures/verify_cert_test3.ca | 0 .../tests}/fixtures/verify_cert_test3.pem | 0 .../tests}/livetest_with_mecas2.py | 13 ++++---- .../tests}/livetest_with_oldpyas2.py | 13 ++++---- {tests => pyas2lib/tests}/test_advanced.py | 8 +++-- {tests => pyas2lib/tests}/test_basic.py | 6 ++-- {tests => pyas2lib/tests}/test_mdn.py | 5 ++-- {tests => pyas2lib/tests}/test_with_mecas2.py | 6 ++-- 37 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 .coveragerc rename {tests => pyas2lib/tests}/__init__.py (95%) rename {tests => pyas2lib/tests}/fixtures/cert_extract_private.cer (100%) rename {tests => pyas2lib/tests}/fixtures/cert_extract_private.pem (100%) rename {tests => pyas2lib/tests}/fixtures/cert_extract_public.cer (100%) rename {tests => pyas2lib/tests}/fixtures/cert_extract_public.pem (100%) rename {tests => pyas2lib/tests}/fixtures/cert_mecas2_public.pem (100%) rename {tests => pyas2lib/tests}/fixtures/cert_oldpyas2_private.pem (100%) rename {tests => pyas2lib/tests}/fixtures/cert_oldpyas2_public.pem (100%) rename {tests => pyas2lib/tests}/fixtures/cert_sb2bi_public.ca (100%) rename {tests => pyas2lib/tests}/fixtures/cert_sb2bi_public.pem (100%) rename {tests => pyas2lib/tests}/fixtures/cert_test.p12 (100%) rename {tests => pyas2lib/tests}/fixtures/cert_test.pem (100%) rename {tests => pyas2lib/tests}/fixtures/cert_test_public.pem (100%) rename {tests => pyas2lib/tests}/fixtures/mecas2_compressed.as2 (100%) rename {tests => pyas2lib/tests}/fixtures/mecas2_compressed_signed_encrypted.as2 (100%) rename {tests => pyas2lib/tests}/fixtures/mecas2_encrypted.as2 (100%) rename {tests => pyas2lib/tests}/fixtures/mecas2_signed.as2 (100%) rename {tests => pyas2lib/tests}/fixtures/mecas2_signed.mdn (100%) rename {tests => pyas2lib/tests}/fixtures/mecas2_signed_encrypted.as2 (100%) rename {tests => pyas2lib/tests}/fixtures/mecas2_unsigned.mdn (100%) rename {tests => pyas2lib/tests}/fixtures/payload.binary (100%) rename {tests => pyas2lib/tests}/fixtures/payload.txt (100%) create mode 100644 pyas2lib/tests/fixtures/payload_dos.txt rename {tests => pyas2lib/tests}/fixtures/sb2bi_signed.mdn (100%) rename {tests => pyas2lib/tests}/fixtures/sb2bi_signed_cmp.msg (100%) rename {tests => pyas2lib/tests}/fixtures/verify_cert_test1.pem (100%) rename {tests => pyas2lib/tests}/fixtures/verify_cert_test2.cer (100%) rename {tests => pyas2lib/tests}/fixtures/verify_cert_test3.ca (100%) rename {tests => pyas2lib/tests}/fixtures/verify_cert_test3.pem (100%) rename {tests => pyas2lib/tests}/livetest_with_mecas2.py (95%) rename {tests => pyas2lib/tests}/livetest_with_oldpyas2.py (95%) rename {tests => pyas2lib/tests}/test_advanced.py (99%) rename {tests => pyas2lib/tests}/test_basic.py (98%) rename {tests => pyas2lib/tests}/test_mdn.py (95%) rename {tests => pyas2lib/tests}/test_with_mecas2.py (97%) diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..feb1b19 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,22 @@ +[run] +branch = True +omit = + */site-packages/* + */tests/* + +[report] +exclude_lines = + + # Don't complain about missing debug-only code: + def __repr__ + def __str__ + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + assert + + # Don't complain if non-runnable code isn't run: + if 0: + pass + if __name__ == .__main__.: diff --git a/.gitignore b/.gitignore index 3ecab8f..934b7fb 100644 --- a/.gitignore +++ b/.gitignore @@ -91,5 +91,4 @@ ENV/ # IDEA .idea .pytest_cache/ -.coverage .DS_Store \ No newline at end of file diff --git a/tests/__init__.py b/pyas2lib/tests/__init__.py similarity index 95% rename from tests/__init__.py rename to pyas2lib/tests/__init__.py index e851695..2ec4403 100644 --- a/tests/__init__.py +++ b/pyas2lib/tests/__init__.py @@ -1,9 +1,5 @@ import unittest import os -import sys -sys.path.insert(0, os.path.abspath('..')) - -from pyas2lib import as2, exceptions, utils class Pyas2TestCase(unittest.TestCase): diff --git a/tests/fixtures/cert_extract_private.cer b/pyas2lib/tests/fixtures/cert_extract_private.cer similarity index 100% rename from tests/fixtures/cert_extract_private.cer rename to pyas2lib/tests/fixtures/cert_extract_private.cer diff --git a/tests/fixtures/cert_extract_private.pem b/pyas2lib/tests/fixtures/cert_extract_private.pem similarity index 100% rename from tests/fixtures/cert_extract_private.pem rename to pyas2lib/tests/fixtures/cert_extract_private.pem diff --git a/tests/fixtures/cert_extract_public.cer b/pyas2lib/tests/fixtures/cert_extract_public.cer similarity index 100% rename from tests/fixtures/cert_extract_public.cer rename to pyas2lib/tests/fixtures/cert_extract_public.cer diff --git a/tests/fixtures/cert_extract_public.pem b/pyas2lib/tests/fixtures/cert_extract_public.pem similarity index 100% rename from tests/fixtures/cert_extract_public.pem rename to pyas2lib/tests/fixtures/cert_extract_public.pem diff --git a/tests/fixtures/cert_mecas2_public.pem b/pyas2lib/tests/fixtures/cert_mecas2_public.pem similarity index 100% rename from tests/fixtures/cert_mecas2_public.pem rename to pyas2lib/tests/fixtures/cert_mecas2_public.pem diff --git a/tests/fixtures/cert_oldpyas2_private.pem b/pyas2lib/tests/fixtures/cert_oldpyas2_private.pem similarity index 100% rename from tests/fixtures/cert_oldpyas2_private.pem rename to pyas2lib/tests/fixtures/cert_oldpyas2_private.pem diff --git a/tests/fixtures/cert_oldpyas2_public.pem b/pyas2lib/tests/fixtures/cert_oldpyas2_public.pem similarity index 100% rename from tests/fixtures/cert_oldpyas2_public.pem rename to pyas2lib/tests/fixtures/cert_oldpyas2_public.pem diff --git a/tests/fixtures/cert_sb2bi_public.ca b/pyas2lib/tests/fixtures/cert_sb2bi_public.ca similarity index 100% rename from tests/fixtures/cert_sb2bi_public.ca rename to pyas2lib/tests/fixtures/cert_sb2bi_public.ca diff --git a/tests/fixtures/cert_sb2bi_public.pem b/pyas2lib/tests/fixtures/cert_sb2bi_public.pem similarity index 100% rename from tests/fixtures/cert_sb2bi_public.pem rename to pyas2lib/tests/fixtures/cert_sb2bi_public.pem diff --git a/tests/fixtures/cert_test.p12 b/pyas2lib/tests/fixtures/cert_test.p12 similarity index 100% rename from tests/fixtures/cert_test.p12 rename to pyas2lib/tests/fixtures/cert_test.p12 diff --git a/tests/fixtures/cert_test.pem b/pyas2lib/tests/fixtures/cert_test.pem similarity index 100% rename from tests/fixtures/cert_test.pem rename to pyas2lib/tests/fixtures/cert_test.pem diff --git a/tests/fixtures/cert_test_public.pem b/pyas2lib/tests/fixtures/cert_test_public.pem similarity index 100% rename from tests/fixtures/cert_test_public.pem rename to pyas2lib/tests/fixtures/cert_test_public.pem diff --git a/tests/fixtures/mecas2_compressed.as2 b/pyas2lib/tests/fixtures/mecas2_compressed.as2 similarity index 100% rename from tests/fixtures/mecas2_compressed.as2 rename to pyas2lib/tests/fixtures/mecas2_compressed.as2 diff --git a/tests/fixtures/mecas2_compressed_signed_encrypted.as2 b/pyas2lib/tests/fixtures/mecas2_compressed_signed_encrypted.as2 similarity index 100% rename from tests/fixtures/mecas2_compressed_signed_encrypted.as2 rename to pyas2lib/tests/fixtures/mecas2_compressed_signed_encrypted.as2 diff --git a/tests/fixtures/mecas2_encrypted.as2 b/pyas2lib/tests/fixtures/mecas2_encrypted.as2 similarity index 100% rename from tests/fixtures/mecas2_encrypted.as2 rename to pyas2lib/tests/fixtures/mecas2_encrypted.as2 diff --git a/tests/fixtures/mecas2_signed.as2 b/pyas2lib/tests/fixtures/mecas2_signed.as2 similarity index 100% rename from tests/fixtures/mecas2_signed.as2 rename to pyas2lib/tests/fixtures/mecas2_signed.as2 diff --git a/tests/fixtures/mecas2_signed.mdn b/pyas2lib/tests/fixtures/mecas2_signed.mdn similarity index 100% rename from tests/fixtures/mecas2_signed.mdn rename to pyas2lib/tests/fixtures/mecas2_signed.mdn diff --git a/tests/fixtures/mecas2_signed_encrypted.as2 b/pyas2lib/tests/fixtures/mecas2_signed_encrypted.as2 similarity index 100% rename from tests/fixtures/mecas2_signed_encrypted.as2 rename to pyas2lib/tests/fixtures/mecas2_signed_encrypted.as2 diff --git a/tests/fixtures/mecas2_unsigned.mdn b/pyas2lib/tests/fixtures/mecas2_unsigned.mdn similarity index 100% rename from tests/fixtures/mecas2_unsigned.mdn rename to pyas2lib/tests/fixtures/mecas2_unsigned.mdn diff --git a/tests/fixtures/payload.binary b/pyas2lib/tests/fixtures/payload.binary similarity index 100% rename from tests/fixtures/payload.binary rename to pyas2lib/tests/fixtures/payload.binary diff --git a/tests/fixtures/payload.txt b/pyas2lib/tests/fixtures/payload.txt similarity index 100% rename from tests/fixtures/payload.txt rename to pyas2lib/tests/fixtures/payload.txt diff --git a/pyas2lib/tests/fixtures/payload_dos.txt b/pyas2lib/tests/fixtures/payload_dos.txt new file mode 100644 index 0000000..379bf8e --- /dev/null +++ b/pyas2lib/tests/fixtures/payload_dos.txt @@ -0,0 +1,28 @@ +UNB+UNOA:2+:14+:14+140407:0910+5++++1+EANCOM' +UNH+1+ORDERS:D:96A:UN:EAN008' +BGM+220+1AA1TEST+9' +DTM+137:20140407:102' +DTM+63:20140421:102' +DTM+64:20140414:102' +RFF+ADE:1234' +RFF+PD:1704' +NAD+BY+5450534000024::9' +NAD+SU+::9' +NAD+DP+5450534000109::9+++++++GB' +NAD+IV+5450534000055::9++AMAZON EU SARL:5 RUE PLAETIS LUXEMBOURG+CO PO BOX 4558+SLOUGH++SL1 0TX+GB' +RFF+VA:GB727255821' +CUX+2:EUR:9' +LIN+1++9783898307529:EN' +QTY+21:5' +PRI+AAA:27.5' +LIN+2++390787706322:UP' +QTY+21:1' +PRI+AAA:10.87' +LIN+3' +PIA+5+3899408268X-39:SA' +QTY+21:3' +PRI+AAA:3.85' +UNS+S' +CNT+2:3' +UNT+26+1' +UNZ+1+5' diff --git a/tests/fixtures/sb2bi_signed.mdn b/pyas2lib/tests/fixtures/sb2bi_signed.mdn similarity index 100% rename from tests/fixtures/sb2bi_signed.mdn rename to pyas2lib/tests/fixtures/sb2bi_signed.mdn diff --git a/tests/fixtures/sb2bi_signed_cmp.msg b/pyas2lib/tests/fixtures/sb2bi_signed_cmp.msg similarity index 100% rename from tests/fixtures/sb2bi_signed_cmp.msg rename to pyas2lib/tests/fixtures/sb2bi_signed_cmp.msg diff --git a/tests/fixtures/verify_cert_test1.pem b/pyas2lib/tests/fixtures/verify_cert_test1.pem similarity index 100% rename from tests/fixtures/verify_cert_test1.pem rename to pyas2lib/tests/fixtures/verify_cert_test1.pem diff --git a/tests/fixtures/verify_cert_test2.cer b/pyas2lib/tests/fixtures/verify_cert_test2.cer similarity index 100% rename from tests/fixtures/verify_cert_test2.cer rename to pyas2lib/tests/fixtures/verify_cert_test2.cer diff --git a/tests/fixtures/verify_cert_test3.ca b/pyas2lib/tests/fixtures/verify_cert_test3.ca similarity index 100% rename from tests/fixtures/verify_cert_test3.ca rename to pyas2lib/tests/fixtures/verify_cert_test3.ca diff --git a/tests/fixtures/verify_cert_test3.pem b/pyas2lib/tests/fixtures/verify_cert_test3.pem similarity index 100% rename from tests/fixtures/verify_cert_test3.pem rename to pyas2lib/tests/fixtures/verify_cert_test3.pem diff --git a/tests/livetest_with_mecas2.py b/pyas2lib/tests/livetest_with_mecas2.py similarity index 95% rename from tests/livetest_with_mecas2.py rename to pyas2lib/tests/livetest_with_mecas2.py index f33928b..e415073 100644 --- a/tests/livetest_with_mecas2.py +++ b/pyas2lib/tests/livetest_with_mecas2.py @@ -1,8 +1,11 @@ -from __future__ import unicode_literals, absolute_import, print_function -from . import as2, Pyas2TestCase -import requests +"""Module for testing with a live mecas2 server.""" import os +import requests + +from pyas2lib import as2 +from . import Pyas2TestCase + TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testdata') @@ -12,9 +15,9 @@ def setUp(self): self.org = as2.Organization( as2_name='pyas2lib', sign_key=self.private_key, - sign_key_pass='test'.encode('utf-8'), + sign_key_pass='test', decrypt_key=self.private_key, - decrypt_key_pass='test'.encode('utf-8') + decrypt_key_pass='test' ) self.partner = as2.Partner( diff --git a/tests/livetest_with_oldpyas2.py b/pyas2lib/tests/livetest_with_oldpyas2.py similarity index 95% rename from tests/livetest_with_oldpyas2.py rename to pyas2lib/tests/livetest_with_oldpyas2.py index 4a9fb64..016e976 100644 --- a/tests/livetest_with_oldpyas2.py +++ b/pyas2lib/tests/livetest_with_oldpyas2.py @@ -1,8 +1,11 @@ -from __future__ import unicode_literals, absolute_import, print_function -from . import as2, Pyas2TestCase -import requests +"""Module for testing with a live old pyas2 server.""" import os +import requests + +from pyas2lib import as2 +from . import Pyas2TestCase + TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testdata') @@ -12,9 +15,9 @@ def setUp(self): self.org = as2.Organization( as2_name='pyas2lib', sign_key=self.private_key, - sign_key_pass='test'.encode('utf-8'), + sign_key_pass='test', decrypt_key=self.private_key, - decrypt_key_pass='test'.encode('utf-8') + decrypt_key_pass='test' ) self.partner = as2.Partner( diff --git a/tests/test_advanced.py b/pyas2lib/tests/test_advanced.py similarity index 99% rename from tests/test_advanced.py rename to pyas2lib/tests/test_advanced.py index 4c021f4..9492f7e 100644 --- a/tests/test_advanced.py +++ b/pyas2lib/tests/test_advanced.py @@ -1,8 +1,10 @@ -from __future__ import unicode_literals, absolute_import, print_function -from . import Pyas2TestCase, as2, utils -import os +"""Module for testing the advanced features of pyas2lib.""" import base64 import datetime +import os + +from pyas2lib import as2, utils +from . import Pyas2TestCase class TestAdvanced(Pyas2TestCase): diff --git a/tests/test_basic.py b/pyas2lib/tests/test_basic.py similarity index 98% rename from tests/test_basic.py rename to pyas2lib/tests/test_basic.py index 1a959e5..1458a04 100644 --- a/tests/test_basic.py +++ b/pyas2lib/tests/test_basic.py @@ -1,5 +1,7 @@ -from __future__ import unicode_literals, absolute_import, print_function -from . import as2, Pyas2TestCase +"""Module for testing the basic features of pyas2.""" + +from pyas2lib import as2 +from . import Pyas2TestCase class TestBasic(Pyas2TestCase): diff --git a/tests/test_mdn.py b/pyas2lib/tests/test_mdn.py similarity index 95% rename from tests/test_mdn.py rename to pyas2lib/tests/test_mdn.py index 5e33457..49f0f7a 100644 --- a/tests/test_mdn.py +++ b/pyas2lib/tests/test_mdn.py @@ -1,5 +1,6 @@ -from __future__ import unicode_literals, absolute_import, print_function -from . import as2, Pyas2TestCase +"""Module for testing the MDN related features of pyas2lib""" +from pyas2lib import as2 +from . import Pyas2TestCase class TestMDN(Pyas2TestCase): diff --git a/tests/test_with_mecas2.py b/pyas2lib/tests/test_with_mecas2.py similarity index 97% rename from tests/test_with_mecas2.py rename to pyas2lib/tests/test_with_mecas2.py index 6a34a02..08d4afd 100644 --- a/tests/test_with_mecas2.py +++ b/pyas2lib/tests/test_with_mecas2.py @@ -1,7 +1,9 @@ -from __future__ import unicode_literals, absolute_import, print_function -from . import Pyas2TestCase, as2 +"""Module for testing with files generated by mendelson as2 server.""" import os +from pyas2lib import as2 +from . import Pyas2TestCase + class TestMecAS2(Pyas2TestCase): From dac1426598c52343841e91b14cf730d521680eef Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 11 Jun 2019 09:38:25 +0530 Subject: [PATCH 28/66] use coverage config file --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4e8d6f0..90461ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ install: - python setup.py install - pip install pytest-cov script: - - pytest --cov=pyas2lib + - pytest --cov=pyas2lib --cov-config .coveragerc after_success: - pip install codecov - codecov \ No newline at end of file From d3726446d1c74cd923797ceeb23534decd22b007 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 11 Jun 2019 15:04:39 +0530 Subject: [PATCH 29/66] multiple fixes and increase the coverage to 95% --- pyas2lib/as2.py | 94 +++++++----------- pyas2lib/cms.py | 100 +++++++++---------- pyas2lib/constants.py | 36 +++++++ pyas2lib/tests/__init__.py | 75 +++++--------- pyas2lib/tests/test_advanced.py | 153 +++++++++++++++++++---------- pyas2lib/tests/test_cms.py | 35 +++++++ pyas2lib/tests/test_utils.py | 84 ++++++++++++++++ pyas2lib/tests/test_with_mecas2.py | 16 +-- pyas2lib/utils.py | 60 ++++++----- 9 files changed, 402 insertions(+), 251 deletions(-) create mode 100644 pyas2lib/constants.py create mode 100644 pyas2lib/tests/test_cms.py create mode 100644 pyas2lib/tests/test_utils.py diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 17596c1..3fb2c32 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -10,46 +10,29 @@ from email.mime.multipart import MIMEMultipart from oscrypto import asymmetric -from pyas2lib.cms import DIGEST_ALGORITHMS -from pyas2lib.cms import ENCRYPTION_ALGORITHMS -from pyas2lib.cms import compress_message -from pyas2lib.cms import decompress_message -from pyas2lib.cms import decrypt_message -from pyas2lib.cms import encrypt_message -from pyas2lib.cms import sign_message -from pyas2lib.cms import verify_message +from pyas2lib.cms import ( + compress_message, + decompress_message, + decrypt_message, + encrypt_message, + sign_message, + verify_message +) +from pyas2lib.constants import * from pyas2lib.exceptions import * -from pyas2lib.utils import canonicalize -from pyas2lib.utils import extract_first_part -from pyas2lib.utils import make_mime_boundary -from pyas2lib.utils import mime_to_bytes -from pyas2lib.utils import pem_to_der -from pyas2lib.utils import quote_as2name -from pyas2lib.utils import split_pem -from pyas2lib.utils import unquote_as2name -from pyas2lib.utils import verify_certificate_chain - -logger = logging.getLogger('pyas2lib') - -AS2_VERSION = '1.2' - -EDIINT_FEATURES = 'CMS' - -IGNORE_SELF_SIGNED_CERTS = True - -SYNCHRONOUS_MDN = 'SYNC' -ASYNCHRONOUS_MDN = 'ASYNC' - -MDN_MODES = ( - SYNCHRONOUS_MDN, - ASYNCHRONOUS_MDN +from pyas2lib.utils import ( + canonicalize, + extract_first_part, + make_mime_boundary, + mime_to_bytes, + pem_to_der, + quote_as2name, + split_pem, + unquote_as2name, + verify_certificate_chain ) -MDN_CONFIRM_TEXT = 'The AS2 message has been successfully processed. ' \ - 'Thank you for exchanging AS2 messages with pyAS2.' - -MDN_FAILED_TEXT = 'The AS2 message could not be processed. The ' \ - 'disposition-notification report has additional details.' +logger = logging.getLogger('pyas2lib') @dataclass @@ -89,8 +72,7 @@ def __post_init__(self): self.sign_key = self.load_key(self.sign_key, self.sign_key_pass) if self.decrypt_key: - self.decrypt_key = self.load_key( - self.decrypt_key, self.decrypt_key_pass) + self.decrypt_key = self.load_key(self.decrypt_key, self.decrypt_key_pass) @staticmethod def load_key(key_str: bytes, key_pass: str): @@ -192,6 +174,7 @@ class Partner(object): mdn_mode: str = None mdn_digest_alg: str = None mdn_confirm_text: str = MDN_CONFIRM_TEXT + ignore_self_signed: bool = True def __post_init__(self): """Run the post initialisation checks for this class.""" @@ -230,7 +213,7 @@ def load_verify_cert(self): # Verify the certificate against the trusted roots verify_certificate_chain( - cert, trust_roots, ignore_self_signed=IGNORE_SELF_SIGNED_CERTS) + cert, trust_roots, ignore_self_signed=self.ignore_self_signed) return asymmetric.load_certificate(self.verify_cert) @@ -247,7 +230,7 @@ def load_encrypt_cert(self): # Verify the certificate against the trusted roots verify_certificate_chain( - cert, trust_roots, ignore_self_signed=IGNORE_SELF_SIGNED_CERTS) + cert, trust_roots, ignore_self_signed=self.ignore_self_signed) return asymmetric.load_certificate(self.encrypt_cert) @@ -281,8 +264,7 @@ def __init__(self, sender=None, receiver=None): @property def content(self): """Function returns the body of the as2 payload as a bytes object""" - - if not self.payload: + if self.payload is None: return '' if self.payload.is_multipart(): @@ -293,8 +275,6 @@ def content(self): return boundary + boundary.join(temp) else: content = self.payload.get_payload(decode=True) - if isinstance(content, str): - content = content.encode('utf-8') return content @property @@ -337,8 +317,7 @@ def build(self, data, filename=None, subject='AS2 Message', """ # Validations - assert type(data) is bytes, \ - 'Parameter data must be of bytes type.' + assert type(data) is bytes, 'Parameter data must be of bytes type.' additional_headers = additional_headers if additional_headers else {} assert type(additional_headers) is dict @@ -558,7 +537,6 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, self.enc_alg, decrypted_content = decrypt_message( encrypted_data, self.receiver.decrypt_key) - raw_content = decrypted_content self.payload = parse_mime(decrypted_content) if self.payload.get_content_type() == 'text/plain': @@ -657,7 +635,7 @@ def __init__(self, mdn_mode=None, digest_alg=None, mdn_url=None): def content(self): """Function returns the body of the mdn message as a byte string""" - if self.payload: + if self.payload is not None: message_bytes = mime_to_bytes( self.payload, 0).replace(b'\n', b'\r\n') boundary = b'--' + self.payload.get_boundary().encode('utf-8') @@ -682,17 +660,20 @@ def headers_str(self): message_header += '{}: {}\r\n'.format(k, v) return message_header.encode('utf-8') - def build(self, message, status, detailed_status=None): + def build(self, message, status, detailed_status=None, confirmation_text=MDN_CONFIRM_TEXT, + failed_text=MDN_FAILED_TEXT): """Function builds and signs an AS2 MDN message. :param message: The received AS2 message for which this is an MDN. :param status: The status of processing of the received AS2 message. - :param detailed_status: - The optional detailed status of processing of the received AS2 - message. Used to give additional error info (default "None") + :param detailed_status: The optional detailed status of processing of the received AS2 + message. Used to give additional error info (default "None") + :param confirmation_text: The confirmation message sent in the first part of the MDN. + + :param failed_text: The failure message sent in the first part of the failed MDN. """ # Generate message id using UUID 1 as it uses both hostname and time @@ -711,8 +692,6 @@ def build(self, message, status, detailed_status=None): } # Set the confirmation text message here - confirmation_text = MDN_CONFIRM_TEXT - # overwrite with organization specific message if message.receiver and message.receiver.mdn_confirm_text: confirmation_text = message.receiver.mdn_confirm_text @@ -722,7 +701,7 @@ def build(self, message, status, detailed_status=None): confirmation_text = message.sender.mdn_confirm_text if status != 'processed': - confirmation_text = MDN_FAILED_TEXT + confirmation_text = failed_text self.payload = MIMEMultipart( 'report', report_type='disposition-notification') @@ -800,8 +779,7 @@ def build(self, message, status, detailed_status=None): self.payload.replace_header(k, v) else: self.payload.add_header(k, v) - if self.payload.is_multipart(): - self.payload.set_boundary(make_mime_boundary()) + self.payload.set_boundary(make_mime_boundary()) def parse(self, raw_content, find_message_cb): """Function parses the RAW AS2 MDN, verifies it and extracts the diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index b40df8b..a82aa83 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -6,23 +6,7 @@ from oscrypto import asymmetric, symmetric, util from pyas2lib.exceptions import * - -DIGEST_ALGORITHMS = ( - 'md5', - 'sha1', - 'sha224', - 'sha256', - 'sha384', - 'sha512' -) -ENCRYPTION_ALGORITHMS = ( - 'tripledes_192_cbc', - 'rc2_128_cbc', - 'rc4_128_cbc' - 'aes_128_cbc', - 'aes_192_cbc', - 'aes_256_cbc', -) +from pyas2lib.constants import DIGEST_ALGORITHMS def compress_message(data_to_compress): @@ -67,8 +51,7 @@ def decompress_message(compressed_data): raise DecompressionError('Compressed data not found in ASN.1 ') except Exception as e: - raise DecompressionError( - 'Decompression failed with cause: {}'.format(e)) + raise DecompressionError('Decompression failed with cause: {}'.format(e)) def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): @@ -85,39 +68,46 @@ def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): enc_alg_list = enc_alg.split('_') cipher, key_length, mode = enc_alg_list[0], enc_alg_list[1], enc_alg_list[2] - algorithm_id, iv, encrypted_content = None, None, None + enc_alg_asn1, encrypted_content = None, None # Generate the symmetric encryption key and encrypt the message key = util.rand_bytes(int(key_length) // 8) if cipher == 'tripledes': algorithm_id = '1.2.840.113549.3.7' - iv, encrypted_content = symmetric.tripledes_cbc_pkcs5_encrypt( - key, data_to_encrypt, None) + iv, encrypted_content = symmetric.tripledes_cbc_pkcs5_encrypt(key, data_to_encrypt, None) + enc_alg_asn1 = algos.EncryptionAlgorithm({ + 'algorithm': algorithm_id, + 'parameters': cms.OctetString(iv) + }) elif cipher == 'rc2': algorithm_id = '1.2.840.113549.3.2' - iv, encrypted_content = symmetric.rc2_cbc_pkcs5_encrypt( - key, data_to_encrypt, None) + iv, encrypted_content = symmetric.rc2_cbc_pkcs5_encrypt(key, data_to_encrypt, None) + enc_alg_asn1 = algos.EncryptionAlgorithm({ + 'algorithm': algorithm_id, + 'parameters': algos.Rc2Params({'iv': cms.OctetString(iv)}) + }) elif cipher == 'rc4': - algorithm_id = '1.2.840.113549.3.4' + algorithm_id = '1.2.840.113549.1.12.1.1' encrypted_content = symmetric.rc4_encrypt(key, data_to_encrypt) + enc_alg_asn1 = algos.EncryptionAlgorithm({ + 'algorithm': algorithm_id, + }) elif cipher == 'aes': if key_length == '128': algorithm_id = '2.16.840.1.101.3.4.1.2' elif key_length == '192': algorithm_id = '2.16.840.1.101.3.4.1.22' - elif key_length == '256': + else: algorithm_id = '2.16.840.1.101.3.4.1.42' - iv, encrypted_content = symmetric.aes_cbc_pkcs7_encrypt( - key, data_to_encrypt, None) - - enc_alg_asn1 = algos.EncryptionAlgorithm({ - 'algorithm': algorithm_id, - 'parameters': cms.OctetString(iv) - }) + iv, encrypted_content = symmetric.aes_cbc_pkcs7_encrypt(key, data_to_encrypt, None) + enc_alg_asn1 = algos.EncryptionAlgorithm({ + 'algorithm': algorithm_id, + 'parameters': cms.OctetString(iv) + }) # Encrypt the key and build the ASN.1 message encrypted_key = asymmetric.rsa_pkcs1v15_encrypt(encryption_cert, key) @@ -175,14 +165,12 @@ def decrypt_message(encrypted_data, decryption_key): if key_enc_alg == 'rsa': try: - key = asymmetric.rsa_pkcs1v15_decrypt( - decryption_key[0], encrypted_key) - except Exception as e: - raise DecryptionError('Failed to decrypt the payload: ' - 'Could not extract decryption key.') - alg = cms_content['content']['encrypted_content_info'][ - 'content_encryption_algorithm'] + key = asymmetric.rsa_pkcs1v15_decrypt(decryption_key[0], encrypted_key) + except Exception: + raise DecryptionError( + 'Failed to decrypt the payload: Could not extract decryption key.') + alg = cms_content['content']['encrypted_content_info']['content_encryption_algorithm'] encapsulated_data = cms_content['content'][ 'encrypted_content_info']['encrypted_content'].native @@ -195,16 +183,18 @@ def decrypt_message(encrypted_data, decryption_key): decrypted_content = symmetric.aes_cbc_pkcs7_decrypt( key, encapsulated_data, alg.encryption_iv) elif alg.encryption_cipher == 'rc4': - decrypted_content = symmetric.rc2_cbc_pkcs5_decrypt( - key, encapsulated_data, alg.encryption_iv) + decrypted_content = symmetric.rc4_decrypt(key, encapsulated_data) elif alg.encryption_cipher == 'rc2': - decrypted_content = symmetric.rc2_cbc_pkcs5_encrypt( - key, encapsulated_data, alg.encryption_iv) + decrypted_content = symmetric.rc2_cbc_pkcs5_decrypt( + key, encapsulated_data, alg['parameters']['iv'].native) else: raise AS2Exception('Unsupported Encryption Algorithm') except Exception as e: - raise DecryptionError( - 'Failed to decrypt the payload: {}'.format(e)) + raise DecryptionError('Failed to decrypt the payload: {}'.format(e)) + else: + raise AS2Exception('Unsupported Encryption Algorithm') + else: + raise DecryptionError('Encrypted data not found in ASN.1 ') return cipher, decrypted_content @@ -288,12 +278,10 @@ class SmimeCapabilities(core.Sequence): ]) }), ]) - signature = asymmetric.rsa_pkcs1v15_sign( - sign_key[0], signed_attributes.dump(), digest_alg) + signature = asymmetric.rsa_pkcs1v15_sign(sign_key[0], signed_attributes.dump(), digest_alg) else: signed_attributes = None - signature = asymmetric.rsa_pkcs1v15_sign( - sign_key[0], data_to_sign, digest_alg) + signature = asymmetric.rsa_pkcs1v15_sign(sign_key[0], data_to_sign, digest_alg) return cms.ContentInfo({ 'content_type': cms.ContentType('signed_data'), @@ -380,22 +368,22 @@ def verify_message(data_to_verify, signature, verify_cert): digest_func.update(data_to_verify) calc_message_digest = digest_func.digest() if message_digest != calc_message_digest: - raise IntegrityError('Failed to verify message signature: ' - 'Message Digest does not match.') + raise IntegrityError( + 'Failed to verify message signature: Message Digest does not match.') signed_data = signed_attributes.untag().dump() try: if sig_alg == 'rsassa_pkcs1v15': - asymmetric.rsa_pkcs1v15_verify( - verify_cert, sig, signed_data, digest_alg) + asymmetric.rsa_pkcs1v15_verify(verify_cert, sig, signed_data, digest_alg) elif sig_alg == 'rsassa_pss': - asymmetric.rsa_pss_verify( - verify_cert, sig, signed_data, digest_alg) + asymmetric.rsa_pss_verify(verify_cert, sig, signed_data, digest_alg) else: raise AS2Exception('Unsupported Signature Algorithm') except Exception as e: raise IntegrityError( 'Failed to verify message signature: {}'.format(e)) + else: + raise IntegrityError('Signed data not found in ASN.1 ') return digest_alg diff --git a/pyas2lib/constants.py b/pyas2lib/constants.py new file mode 100644 index 0000000..5c5f0c1 --- /dev/null +++ b/pyas2lib/constants.py @@ -0,0 +1,36 @@ +"""Module for defining the constants used by pyas2lib""" + +AS2_VERSION = '1.2' + +EDIINT_FEATURES = 'CMS' + +SYNCHRONOUS_MDN = 'SYNC' +ASYNCHRONOUS_MDN = 'ASYNC' + +MDN_MODES = ( + SYNCHRONOUS_MDN, + ASYNCHRONOUS_MDN +) + +MDN_CONFIRM_TEXT = 'The AS2 message has been successfully processed. ' \ + 'Thank you for exchanging AS2 messages with pyAS2.' + +MDN_FAILED_TEXT = 'The AS2 message could not be processed. The ' \ + 'disposition-notification report has additional details.' + +DIGEST_ALGORITHMS = ( + 'md5', + 'sha1', + 'sha224', + 'sha256', + 'sha384', + 'sha512' +) +ENCRYPTION_ALGORITHMS = ( + 'tripledes_192_cbc', + 'rc2_128_cbc', + 'rc4_128_cbc' + 'aes_128_cbc', + 'aes_192_cbc', + 'aes_256_cbc', +) diff --git a/pyas2lib/tests/__init__.py b/pyas2lib/tests/__init__.py index 2ec4403..a72a2fa 100644 --- a/pyas2lib/tests/__init__.py +++ b/pyas2lib/tests/__init__.py @@ -1,60 +1,29 @@ import unittest import os +TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fixtures') -class Pyas2TestCase(unittest.TestCase): - TEST_DIR = os.path.join( - os.path.dirname(os.path.abspath(__file__)), 'fixtures') +class Pyas2TestCase(unittest.TestCase): + @classmethod def setUpClass(cls): - with open(os.path.join(cls.TEST_DIR, 'payload.txt'), 'rb') as t_file: - cls.test_data = t_file.read() - - with open(os.path.join( - cls.TEST_DIR, 'cert_test.p12'), 'rb') as fp: - cls.private_key = fp.read() - - with open(os.path.join( - cls.TEST_DIR, 'cert_test_public.pem'), 'rb') as fp: - cls.public_key = fp.read() - - with open(os.path.join( - cls.TEST_DIR, 'cert_mecas2_public.pem'), 'rb') as fp: - cls.mecas2_public_key = fp.read() - - with open(os.path.join( - cls.TEST_DIR, 'cert_oldpyas2_public.pem'), 'rb') as fp: - cls.oldpyas2_public_key = fp.read() - - with open(os.path.join( - cls.TEST_DIR, 'cert_oldpyas2_public.pem'), 'rb') as fp: - cls.oldpyas2_public_key = fp.read() - - with open(os.path.join( - cls.TEST_DIR, 'cert_oldpyas2_private.pem'), 'rb') as fp: - cls.oldpyas2_private_key = fp.read() - - with open(os.path.join( - cls.TEST_DIR, 'cert_sb2bi_public.pem'), 'rb') as fp: - cls.sb2bi_public_key = fp.read() - - 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() + """Perform the setup actions for the test case.""" + file_list = { + 'test_data': 'payload.txt', + 'private_key': 'cert_test.p12', + 'public_key': 'cert_test_public.pem', + 'mecas2_public_key': 'cert_mecas2_public.pem', + 'oldpyas2_public_key': 'cert_oldpyas2_public.pem', + 'oldpyas2_private_key': 'cert_oldpyas2_private.pem', + 'sb2bi_public_key': 'cert_sb2bi_public.pem', + 'sb2bi_public_ca': 'cert_sb2bi_public.ca', + 'private_cer': 'cert_extract_private.cer', + 'private_pem': 'cert_extract_private.pem', + + } + + # Load the files to the attrs + for attr, filename in file_list.items(): + with open(os.path.join(TEST_DIR, filename), 'rb') as fp: + setattr(cls, attr, fp.read()) diff --git a/pyas2lib/tests/test_advanced.py b/pyas2lib/tests/test_advanced.py index 9492f7e..be776af 100644 --- a/pyas2lib/tests/test_advanced.py +++ b/pyas2lib/tests/test_advanced.py @@ -1,10 +1,11 @@ """Module for testing the advanced features of pyas2lib.""" import base64 -import datetime import os +from email import message -from pyas2lib import as2, utils -from . import Pyas2TestCase +from pyas2lib import as2 +from pyas2lib.exceptions import ImproperlyConfigured +from pyas2lib.tests import Pyas2TestCase, TEST_DIR class TestAdvanced(Pyas2TestCase): @@ -31,7 +32,7 @@ def test_binary_message(self): self.partner.encrypt = True self.partner.compress = True out_message = as2.Message(self.org, self.partner) - test_message_path = os.path.join(self.TEST_DIR, 'payload.binary') + test_message_path = os.path.join(TEST_DIR, 'payload.binary') with open(test_message_path, 'rb') as bin_file: original_message = bin_file.read() out_message.build( @@ -170,25 +171,31 @@ def test_insufficient_security(self): self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - self.partner.sign = True self.partner.encrypt = True raw_out_message = \ self.out_message.headers_str + b'\r\n' + self.out_message.content in_message = as2.Message() - _, _, mdn = in_message.parse( + status, (exc, _), mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner, find_message_cb=lambda x, y: False ) + self.assertEqual(status, 'processed/Error') + self.assertEqual(exc.disposition_modifier, 'insufficient-message-security') - out_mdn = as2.Mdn() - status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + # Try again for signing check + self.partner.encrypt = False + self.partner.sign = True + in_message = as2.Message() + status, (exc, _), mdn = in_message.parse( + raw_out_message, + find_org_cb=self.find_org, + find_partner_cb=self.find_partner, + find_message_cb=lambda x, y: False ) self.assertEqual(status, 'processed/Error') - self.assertEqual(detailed_status, 'insufficient-message-security') + self.assertEqual(exc.disposition_modifier, 'insufficient-message-security') def test_failed_decryption(self): """ Test case where message decryption has failed """ @@ -254,7 +261,7 @@ def test_verify_certificate(self): """ Test case where we have try to load an expired cert """ # First test with a certificate with invalid root - cert_path = os.path.join(self.TEST_DIR, 'verify_cert_test1.pem') + cert_path = os.path.join(TEST_DIR, 'verify_cert_test1.pem') with open(cert_path, 'rb') as cert_file: try: as2.Partner( @@ -266,7 +273,7 @@ def test_verify_certificate(self): 'unable to get local issuer certificate', str(e)) # Test with an expired certificate - cert_path = os.path.join(self.TEST_DIR, 'verify_cert_test2.cer') + cert_path = os.path.join(TEST_DIR, 'verify_cert_test2.cer') with open(cert_path, 'rb') as cert_file: try: as2.Partner( @@ -278,7 +285,7 @@ def test_verify_certificate(self): 'certificate has expired', str(e)) # Test with a chain certificate - cert_path = os.path.join(self.TEST_DIR, 'verify_cert_test3.pem') + cert_path = os.path.join(TEST_DIR, 'verify_cert_test3.pem') with open(cert_path, 'rb') as cert_file: try: as2.Partner( @@ -290,7 +297,7 @@ def test_verify_certificate(self): 'unable to get local issuer certificate', str(e)) # Test chain certificate with the ca - cert_ca_path = os.path.join(self.TEST_DIR, 'verify_cert_test3.ca') + cert_ca_path = os.path.join(TEST_DIR, 'verify_cert_test3.ca') with open(cert_path, 'rb') as cert_file: with open(cert_ca_path, 'rb') as cert_ca_file: try: @@ -306,7 +313,7 @@ def test_load_private_key(self): """ Test case where we have try to load keys in different formats """ # First test with a pkcs12 key file - cert_path = os.path.join(self.TEST_DIR, 'cert_test.p12') + cert_path = os.path.join(TEST_DIR, 'cert_test.p12') with open(cert_path, 'rb') as cert_file: try: as2.Organization( @@ -318,7 +325,7 @@ def test_load_private_key(self): self.fail('Failed to load p12 private key: %s' % e) # Now test with a pem encoded key file - cert_path = os.path.join(self.TEST_DIR, 'cert_test.pem') + cert_path = os.path.join(TEST_DIR, 'cert_test.pem') with open(cert_path, 'rb') as cert_file: try: as2.Organization( @@ -329,37 +336,81 @@ 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 test_partner_checks(self): + """Test the checks for the partner on initialization.""" + with self.assertRaises(ImproperlyConfigured): + as2.Partner('a partner', digest_alg='xyz') + + with self.assertRaises(ImproperlyConfigured): + as2.Partner('a partner', enc_alg='xyz') + + with self.assertRaises(ImproperlyConfigured): + as2.Partner('a partner', mdn_mode='xyz') + + with self.assertRaises(ImproperlyConfigured): + as2.Partner('a partner', mdn_digest_alg='xyz') + + def test_message_checks(self): + """Test the checks and other features of Message.""" + msg = as2.Message() + assert msg.content == '' + assert msg.headers == {} + assert msg.headers_str == b'' + + msg.payload = message.Message() + msg.payload.set_payload(b'data') + assert msg.content == b'data' + + org = as2.Organization(as2_name='AS2 Server') + partner = as2.Partner(as2_name='AS2 Partner', sign=True) + msg = as2.Message(sender=org, receiver=partner) + with self.assertRaises(ImproperlyConfigured): + msg.build(b'data') + + msg.receiver.sign = False + msg.receiver.encrypt = True + with self.assertRaises(ImproperlyConfigured): + msg.build(b'data') + + msg.receiver.encrypt = False + msg.receiver.mdn_mode = 'ASYNC' + with self.assertRaises(ImproperlyConfigured): + msg.build(b'data') + + msg.sender.mdn_url = 'http://localhost/pyas2/as2receive' + msg.build(b'data') + + def test_mdn_checks(self): + """Test the checks and other features of MDN.""" + mdn = as2.Mdn() + assert mdn.content == '' + assert mdn.headers == {} + assert mdn.headers_str == b'' + + def test_all_encryption_algos(self): + """Test all the available encryption algorithms.""" + algos = ['rc2_128_cbc', 'rc4_128_cbc', 'aes_128_cbc', 'aes_192_cbc', 'aes_256_cbc'] + + for algo in algos: + # Build an As2 message to be transmitted to partner + self.partner.encrypt = True + self.partner.enc_alg = algo + out_message = as2.Message(self.org, self.partner) + out_message.build(self.test_data) + raw_out_message = out_message.headers_str + b'\r\n' + out_message.content + + # Parse the generated AS2 message as the partner + in_message = as2.Message() + status, _, _ = in_message.parse( + raw_out_message, + find_org_cb=self.find_org, + find_partner_cb=self.find_partner + ) + + # Compare the mic contents of the input and output messages + self.assertEqual(status, 'processed') + self.assertTrue(in_message.encrypted) + self.assertEqual(self.test_data, in_message.content) def find_org(self, headers): return self.org @@ -388,11 +439,13 @@ def setUp(self): encrypt_cert=self.sb2bi_public_key, encrypt_cert_ca=self.sb2bi_public_ca, ) + self.partner.load_verify_cert() + self.partner.load_encrypt_cert() def xtest_process_message(self): """ Test processing message received from Sterling Integrator""" with open(os.path.join( - self.TEST_DIR, 'sb2bi_signed_cmp.msg'), 'rb') as msg: + TEST_DIR, 'sb2bi_signed_cmp.msg'), 'rb') as msg: as2message = as2.Message() status, exception, as2mdn = as2message.parse( msg.read(), @@ -411,7 +464,7 @@ def test_process_mdn(self): as2mdn = as2.Mdn() # Parse the mdn and get the message status with open(os.path.join( - self.TEST_DIR, 'sb2bi_signed.mdn'), 'rb') as mdn: + TEST_DIR, 'sb2bi_signed.mdn'), 'rb') as mdn: status, detailed_status = as2mdn.parse( mdn.read(), lambda x, y: message) self.assertEqual(status, 'processed') diff --git a/pyas2lib/tests/test_cms.py b/pyas2lib/tests/test_cms.py new file mode 100644 index 0000000..319c8d7 --- /dev/null +++ b/pyas2lib/tests/test_cms.py @@ -0,0 +1,35 @@ +"""Module to test cms related features of pyas2lib.""" +import pytest + +from pyas2lib import cms +from pyas2lib.exceptions import ( + DecompressionError, + DecryptionError, + IntegrityError +) + + +INVALID_DATA = cms.cms.ContentInfo({ + 'content_type': cms.cms.ContentType('data'), +}).dump() + + +def test_compress(): + """Test the compression and decompression functions.""" + compressed_data = cms.compress_message(b'data') + assert cms.decompress_message(compressed_data) == b'data' + + with pytest.raises(DecompressionError): + cms.decompress_message(INVALID_DATA) + + +def test_signing(): + """Test the signing and verification functions.""" + with pytest.raises(IntegrityError): + cms.verify_message(b'data', INVALID_DATA, None) + + +def test_encryption(): + """Test the encryption and decryption functions.""" + with pytest.raises(DecryptionError): + cms.decrypt_message(INVALID_DATA, None) diff --git a/pyas2lib/tests/test_utils.py b/pyas2lib/tests/test_utils.py new file mode 100644 index 0000000..072a528 --- /dev/null +++ b/pyas2lib/tests/test_utils.py @@ -0,0 +1,84 @@ +"""Module to test the utility functions of pyas2lib.""" +import datetime +import os +import pytest +from email.message import Message + +from pyas2lib import utils +from pyas2lib.exceptions import AS2Exception +from pyas2lib.tests import TEST_DIR + + +def test_quoting(): + """Test the function for quoting and as2 name.""" + assert utils.quote_as2name('PYAS2LIB') == 'PYAS2LIB' + assert utils.quote_as2name('PYAS2 LIB') == '"PYAS2 LIB"' + + +def test_bytes_generator(): + """Test the email bytes generator class.""" + message = Message() + message.set_type('application/pkcs7-mime') + assert utils.mime_to_bytes(message, 0) == b'MIME-Version: 1.0\n' \ + b'Content-Type: application/pkcs7-mime\n\n' + + +def test_make_boundary(): + """Test the function for creating a boundary for multipart messages.""" + assert utils.make_mime_boundary(text='123456') is not None + + +def test_extract_first_part(): + """Test the function for extracting the first part of a multipart message.""" + message = b'header----first_part\n----second_part\n' + assert utils.extract_first_part(message, b'----') == b'first_part' + + message = b'header----first_part\r\n----second_part\r\n' + assert utils.extract_first_part(message, b'----') == b'first_part' + + +def test_cert_verification(): + """Test the verification of a certificate chain.""" + with open(os.path.join(TEST_DIR, 'cert_sb2bi_public.pem'), 'rb') as fp: + certificate = utils.pem_to_der(fp.read(), return_multiple=False) + + with pytest.raises(AS2Exception): + utils.verify_certificate_chain( + certificate, trusted_certs=[], ignore_self_signed=False) + + +def test_extract_certificate_info(): + """ 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. + with open(os.path.join(TEST_DIR, 'cert_extract_private.cer'), 'rb') as fp: + assert utils.extract_certificate_info(fp.read()) == cert_info + + with open(os.path.join(TEST_DIR, 'cert_extract_private.pem'), 'rb') as fp: + assert utils.extract_certificate_info(fp.read()) == cert_info + + with open(os.path.join(TEST_DIR, 'cert_extract_public.cer'), 'rb') as fp: + assert utils.extract_certificate_info(fp.read()) == cert_info + + with open(os.path.join(TEST_DIR, 'cert_extract_public.pem'), 'rb') as fp: + assert utils.extract_certificate_info(fp.read()) == cert_info + + assert utils.extract_certificate_info(b'') == cert_empty diff --git a/pyas2lib/tests/test_with_mecas2.py b/pyas2lib/tests/test_with_mecas2.py index 08d4afd..bcf58d8 100644 --- a/pyas2lib/tests/test_with_mecas2.py +++ b/pyas2lib/tests/test_with_mecas2.py @@ -2,7 +2,7 @@ import os from pyas2lib import as2 -from . import Pyas2TestCase +from pyas2lib.tests import Pyas2TestCase, TEST_DIR class TestMecAS2(Pyas2TestCase): @@ -26,7 +26,7 @@ def test_compressed_message(self): """ Test Compressed Message received from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join(self.TEST_DIR, 'mecas2_compressed.as2') + received_file = os.path.join(TEST_DIR, 'mecas2_compressed.as2') with open(received_file, 'rb') as fp: in_message = as2.Message() in_message.parse( @@ -43,7 +43,7 @@ def test_encrypted_message(self): """ Test Encrypted Message received from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join(self.TEST_DIR, 'mecas2_encrypted.as2') + received_file = os.path.join(TEST_DIR, 'mecas2_encrypted.as2') with open(received_file, 'rb') as fp: in_message = as2.Message() in_message.parse( @@ -60,7 +60,7 @@ def test_encrypted_message(self): def test_signed_message(self): """ Test Unencrypted Signed Uncompressed Message from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join(self.TEST_DIR, 'mecas2_signed.as2') + received_file = os.path.join(TEST_DIR, 'mecas2_signed.as2') with open(received_file, 'rb') as fp: in_message = as2.Message() in_message.parse( @@ -79,7 +79,7 @@ def test_encrypted_signed_message(self): # Parse the generated AS2 message as the partner received_file = os.path.join( - self.TEST_DIR, 'mecas2_signed_encrypted.as2') + TEST_DIR, 'mecas2_signed_encrypted.as2') with open(received_file, 'rb') as fp: in_message = as2.Message() in_message.parse( @@ -100,7 +100,7 @@ def test_encrypted_signed_compressed_message(self): # Parse the generated AS2 message as the partner received_file = os.path.join( - self.TEST_DIR, 'mecas2_compressed_signed_encrypted.as2') + TEST_DIR, 'mecas2_compressed_signed_encrypted.as2') with open(received_file, 'rb') as fp: in_message = as2.Message() in_message.parse( @@ -120,7 +120,7 @@ def test_unsigned_mdn(self): """ Test Unsigned MDN received from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join(self.TEST_DIR, 'mecas2_unsigned.mdn') + received_file = os.path.join(TEST_DIR, 'mecas2_unsigned.mdn') with open(received_file, 'rb') as fp: in_message = as2.Mdn() status, detailed_status = in_message.parse( @@ -133,7 +133,7 @@ def test_signed_mdn(self): """ Test Signed MDN received from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join(self.TEST_DIR, 'mecas2_signed.mdn') + received_file = os.path.join(TEST_DIR, 'mecas2_signed.mdn') with open(received_file, 'rb') as fp: in_message = as2.Mdn() in_message.parse(fp.read(), find_message_cb=self.find_message) diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index d289795..920611f 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -11,9 +11,9 @@ from datetime import datetime -def unquote_as2name(quoted_name): +def unquote_as2name(quoted_name: str): """ - Function converts as2 name from quoted to unquoted format + Function converts as2 name from quoted to unquoted format. :param quoted_name: the as2 name in quoted format :return: the as2 name in unquoted format @@ -21,9 +21,10 @@ def unquote_as2name(quoted_name): return email.utils.unquote(quoted_name) -def quote_as2name(unquoted_name): +def quote_as2name(unquoted_name: str): """ - Function converts as2 name from unquoted to quoted format + Function converts as2 name from unquoted to quoted format. + :param unquoted_name: the as2 name in unquoted format :return: the as2 name in unquoted format """ @@ -35,11 +36,13 @@ def quote_as2name(unquoted_name): class BinaryBytesGenerator(BytesGenerator): - """ Override the bytes generator to better handle binary data """ + """Override the bytes generator to better handle binary data.""" - def _handle_application_pkcs7_mime(self, msg): - """ Handle writing the binary messages to prevent default behaviour of - newline replacements """ + def _handle_application_pkcs7_mime(self, msg: email.message.Message): + """ + Handle writing the binary messages to prevent default behaviour of + newline replacements. + """ payload = msg.get_payload(decode=True) if payload is None: return @@ -47,9 +50,10 @@ def _handle_application_pkcs7_mime(self, msg): self._fp.write(payload) -def mime_to_bytes(msg, header_len): +def mime_to_bytes(msg: email.message.Message, header_len): """ - Function to convert and email Message to flat string format + Function to convert and email Message to flat string format. + :param msg: email.Message to be converted to string :param header_len: the msx length of the header per line :return: the byte string representation of the email message @@ -60,9 +64,9 @@ def mime_to_bytes(msg, header_len): return fp.getvalue() -def canonicalize(message): +def canonicalize(message: email.message.Message): """ - Function to convert an email Message to standard format string + Function to convert an email Message to standard format string/ :param message: email.Message to be converted to standard string :return: the standard representation of the email message in bytes @@ -80,9 +84,11 @@ def canonicalize(message): b'\r\n', b'\n').replace(b'\r', b'\n').replace(b'\n', b'\r\n') -def make_mime_boundary(text=None): - # Craft a random boundary. If text is given, ensure that the chosen - # boundary doesn't appear in the text. +def make_mime_boundary(text: str = None): + """ + Craft a random boundary. If text is given, ensure that the chosen + boundary doesn't appear in the text. + """ width = len(repr(sys.maxsize - 1)) fmt = '%%0%dd' % width @@ -102,8 +108,8 @@ def make_mime_boundary(text=None): return b -def extract_first_part(message, boundary): - """ Function to extract the first part of a multipart message""" +def extract_first_part(message: bytes, boundary: bytes): + """Function to extract the first part of a multipart message.""" first_message = message.split(boundary)[1].lstrip() if first_message.endswith(b'\r\n'): first_message = first_message[:-2] @@ -112,8 +118,8 @@ def extract_first_part(message, boundary): return first_message -def pem_to_der(cert, return_multiple=True): - """ Converts a given certificate or list to PEM format""" +def pem_to_der(cert: bytes, return_multiple: bool = True): + """Converts a given certificate or list to PEM format.""" # initialize the certificate array cert_list = [] @@ -132,9 +138,10 @@ def pem_to_der(cert, return_multiple=True): return cert_list.pop() -def split_pem(pem_bytes): +def split_pem(pem_bytes: bytes): """ - Split a give PEM file with multiple certificates + Split a give PEM file with multiple certificates. + :param pem_bytes: The pem data in bytes with multiple certs :return: yields a list of certificates contained in the pem file """ @@ -158,11 +165,11 @@ def split_pem(pem_bytes): pem_data = pem_data + line + b'\r\n' -def verify_certificate_chain(cert_str, trusted_certs, ignore_self_signed=True): - """ Verify a given certificate against a trust store""" +def verify_certificate_chain(cert_bytes, trusted_certs, ignore_self_signed=True): + """Verify a given certificate against a trust store.""" # Load the certificate - certificate = crypto.load_certificate(crypto.FILETYPE_ASN1, cert_str) + certificate = crypto.load_certificate(crypto.FILETYPE_ASN1, cert_bytes) # Create a certificate store and add your trusted certs try: @@ -185,10 +192,11 @@ def verify_certificate_chain(cert_str, trusted_certs, ignore_self_signed=True): return True except crypto.X509StoreContextError as e: - raise AS2Exception('Partner Certificate Invalid: %s' % e.args[-1][-1]) + raise AS2Exception( + 'Partner Certificate Invalid: %s' % e.args[-1][-1], 'invalid-certificate') -def extract_certificate_info(cert): +def extract_certificate_info(cert: bytes): """ Extract validity information from the certificate and return a dictionary. From 5f0c973b91d70d03f0945cecc8bbba577a9d16f9 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 11 Jun 2019 15:40:48 +0530 Subject: [PATCH 30/66] correct the algo id for RC4 --- pyas2lib/cms.py | 8 ++++---- pyas2lib/constants.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index a82aa83..0c37763 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -89,7 +89,7 @@ def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): }) elif cipher == 'rc4': - algorithm_id = '1.2.840.113549.1.12.1.1' + algorithm_id = '1.2.840.113549.3.4' encrypted_content = symmetric.rc4_encrypt(key, data_to_encrypt) enc_alg_asn1 = algos.EncryptionAlgorithm({ 'algorithm': algorithm_id, @@ -175,15 +175,15 @@ def decrypt_message(encrypted_data, decryption_key): 'encrypted_content_info']['encrypted_content'].native try: - if alg.encryption_cipher == 'tripledes': + if alg['algorithm'].native == '1.2.840.113549.3.4': # This is RC4 + decrypted_content = symmetric.rc4_decrypt(key, encapsulated_data) + elif alg.encryption_cipher == 'tripledes': cipher = 'tripledes_192_cbc' decrypted_content = symmetric.tripledes_cbc_pkcs5_decrypt( key, encapsulated_data, alg.encryption_iv) elif alg.encryption_cipher == 'aes': decrypted_content = symmetric.aes_cbc_pkcs7_decrypt( key, encapsulated_data, alg.encryption_iv) - elif alg.encryption_cipher == 'rc4': - decrypted_content = symmetric.rc4_decrypt(key, encapsulated_data) elif alg.encryption_cipher == 'rc2': decrypted_content = symmetric.rc2_cbc_pkcs5_decrypt( key, encapsulated_data, alg['parameters']['iv'].native) diff --git a/pyas2lib/constants.py b/pyas2lib/constants.py index 5c5f0c1..0c2edd4 100644 --- a/pyas2lib/constants.py +++ b/pyas2lib/constants.py @@ -29,7 +29,7 @@ ENCRYPTION_ALGORITHMS = ( 'tripledes_192_cbc', 'rc2_128_cbc', - 'rc4_128_cbc' + 'rc4_128_cbc', 'aes_128_cbc', 'aes_192_cbc', 'aes_256_cbc', From 4c3b446c597f72b38cb8acdfd7bbfe6aa879e6f2 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Wed, 12 Jun 2019 09:55:25 +0530 Subject: [PATCH 31/66] use http policy when flattening messages for cleaner handling --- pyas2lib/as2.py | 78 ++++++++++++++------------------- pyas2lib/tests/__init__.py | 1 + pyas2lib/tests/test_advanced.py | 15 +++---- pyas2lib/tests/test_basic.py | 35 ++++++++++++--- pyas2lib/tests/test_utils.py | 4 +- pyas2lib/utils.py | 21 ++++----- 6 files changed, 82 insertions(+), 72 deletions(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 3fb2c32..6f96bc7 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -268,7 +268,7 @@ def content(self): return '' if self.payload.is_multipart(): - message_bytes = mime_to_bytes(self.payload, 0) + message_bytes = mime_to_bytes(self.payload) boundary = b'--' + self.payload.get_boundary().encode('utf-8') temp = message_bytes.split(boundary) temp.pop(0) @@ -343,8 +343,7 @@ def build(self, data, filename=None, subject='AS2 Message', 'AS2-From': quote_as2name(self.sender.as2_name), 'AS2-To': quote_as2name(self.receiver.as2_name), 'Subject': subject, - 'Date': email_utils.formatdate(localtime=True), - # 'recipient-address': message.partner.target_url, + 'Date': email_utils.formatdate(localtime=True) } as2_headers.update(additional_headers) @@ -368,10 +367,8 @@ def build(self, data, filename=None, subject='AS2 Message', compressed_message.set_param('smime-type', 'compressed-data') compressed_message.add_header( 'Content-Disposition', 'attachment', filename='smime.p7z') - compressed_message.add_header( - 'Content-Transfer-Encoding', 'binary') - compressed_message.set_payload( - compress_message(mime_to_bytes(self.payload, 0))) + compressed_message.add_header('Content-Transfer-Encoding', 'binary') + compressed_message.set_payload(compress_message(mime_to_bytes(self.payload))) self.payload = compressed_message @@ -408,7 +405,7 @@ def build(self, data, filename=None, subject='AS2 Message', self.payload = signed_message logger.debug('Signed message %s payload as:\n%s' % ( - self.message_id, mime_to_bytes(self.payload, 0))) + self.message_id, mime_to_bytes(self.payload))) if self.receiver.encrypt: self.encrypted, self.enc_alg = True, self.receiver.enc_alg @@ -421,7 +418,7 @@ def build(self, data, filename=None, subject='AS2 Message', encrypted_message.add_header('Content-Transfer-Encoding', 'binary') encrypt_cert = self.receiver.load_encrypt_cert() encrypted_message.set_payload(encrypt_message( - mime_to_bytes(self.payload, 0), self.enc_alg, encrypt_cert)) + mime_to_bytes(self.payload), self.enc_alg, encrypt_cert)) self.payload = encrypted_message logger.debug('Encrypted message %s payload as:\n%s' % ( @@ -636,8 +633,7 @@ def content(self): """Function returns the body of the mdn message as a byte string""" if self.payload is not None: - message_bytes = mime_to_bytes( - self.payload, 0).replace(b'\n', b'\r\n') + message_bytes = mime_to_bytes(self.payload) boundary = b'--' + self.payload.get_boundary().encode('utf-8') temp = message_bytes.split(boundary) temp.pop(0) @@ -703,12 +699,11 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON if status != 'processed': confirmation_text = failed_text - self.payload = MIMEMultipart( - 'report', report_type='disposition-notification') + self.payload = MIMEMultipart('report', report_type='disposition-notification') # Create and attach the MDN Text Message mdn_text = email_message.Message() - mdn_text.set_payload('%s\n' % confirmation_text) + mdn_text.set_payload('%s\r\n' % confirmation_text) mdn_text.set_type('text/plain') del mdn_text['MIME-Version'] encoders.encode_7or8bit(mdn_text) @@ -717,19 +712,19 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON # Create and attache the MDN Report Message mdn_base = email_message.Message() mdn_base.set_type('message/disposition-notification') - mdn_report = 'Reporting-UA: pyAS2 Open Source AS2 Software\n' - mdn_report += 'Original-Recipient: rfc822; {}\n'.format( + mdn_report = 'Reporting-UA: pyAS2 Open Source AS2 Software\r\n' + mdn_report += 'Original-Recipient: rfc822; {}\r\n'.format( message.headers.get('as2-to')) - mdn_report += 'Final-Recipient: rfc822; {}\n'.format( + mdn_report += 'Final-Recipient: rfc822; {}\r\n'.format( message.headers.get('as2-to')) - mdn_report += 'Original-Message-ID: <{}>\n'.format(message.message_id) + mdn_report += 'Original-Message-ID: <{}>\r\n'.format(message.message_id) mdn_report += 'Disposition: automatic-action/' \ 'MDN-sent-automatically; {}'.format(status) if detailed_status: mdn_report += ': {}'.format(detailed_status) - mdn_report += '\n' + mdn_report += '\r\n' if message.mic: - mdn_report += 'Received-content-MIC: {}, {}\n'.format( + mdn_report += 'Received-content-MIC: {}, {}\r\n'.format( message.mic.decode(), message.digest_alg) mdn_base.set_payload(mdn_report) del mdn_base['MIME-Version'] @@ -743,10 +738,9 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON if message.headers.get('disposition-notification-options') and \ message.receiver and message.receiver.sign_key: self.digest_alg = \ - message.headers['disposition-notification-options'].split( - ';')[-1].split(',')[-1].strip().replace('-', '') - signed_mdn = MIMEMultipart( - 'signed', protocol="application/pkcs7-signature") + message.headers['disposition-notification-options'].\ + split(';')[-1].split(',')[-1].strip().replace('-', '') + signed_mdn = MIMEMultipart('signed', protocol="application/pkcs7-signature") del signed_mdn['MIME-Version'] signed_mdn.attach(self.payload) @@ -755,22 +749,20 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON signature.set_type('application/pkcs7-signature') signature.set_param('name', 'smime.p7s') signature.set_param('smime-type', 'signed-data') - signature.add_header( - 'Content-Disposition', 'attachment', filename='smime.p7s') + signature.add_header('Content-Disposition', 'attachment', filename='smime.p7s') del signature['MIME-Version'] - signature.set_payload(sign_message( - canonicalize(self.payload), - self.digest_alg, - message.receiver.sign_key - )) + signed_data = sign_message( + canonicalize(self.payload), self.digest_alg, message.receiver.sign_key + ) + signature.set_payload(signed_data) encoders.encode_base64(signature) signed_mdn.set_param('micalg', self.digest_alg) signed_mdn.attach(signature) self.payload = signed_mdn - logger.debug('Signature for MDN %s created:\n%s' % ( + logger.debug(f'Signature for MDN %s created:\n%s' % ( message.message_id, signature.as_string())) # Update the headers of the final payload and set message boundary @@ -822,8 +814,7 @@ def parse(self, raw_content, find_message_cb): return status, detailed_status if self.payload.get_content_type() == 'multipart/signed': - message_boundary = ('--' + self.payload.get_boundary()).\ - encode('utf-8') + message_boundary = ('--' + self.payload.get_boundary()).encode('utf-8') # Extract the signature and the signed payload signature = None @@ -840,29 +831,24 @@ def parse(self, raw_content, find_message_cb): mic_content = extract_first_part(raw_content, message_boundary) verify_cert = orig_message.receiver.load_verify_cert() try: - self.digest_alg = verify_message( - mic_content, signature, verify_cert) + self.digest_alg = verify_message(mic_content, signature, verify_cert) except IntegrityError: mic_content = canonicalize(self.payload) - self.digest_alg = verify_message( - mic_content, signature, verify_cert) + self.digest_alg = verify_message(mic_content, signature, verify_cert) for part in self.payload.walk(): if part.get_content_type() == 'message/disposition-notification': - logger.debug('Found MDN report for message %s:\n%s' % ( - orig_message.message_id, part.as_string())) + logger.debug( + f'Found MDN report for message {orig_message.message_id}:\n{part.as_string()}') mdn = part.get_payload()[-1] - mdn_status = mdn['Disposition'].split(';').\ - pop().strip().split(':') + mdn_status = mdn['Disposition'].split(';').pop().strip().split(':') status = mdn_status[0] if status == 'processed': - mdn_mic = mdn.get('Received-Content-MIC', '').\ - split(',')[0] + mdn_mic = mdn.get('Received-Content-MIC', '').split(',')[0] # TODO: Check MIC for all cases - if mdn_mic and orig_message.mic \ - and mdn_mic != orig_message.mic.decode(): + if mdn_mic and orig_message.mic and mdn_mic != orig_message.mic.decode(): status = 'processed/warning' detailed_status = 'Message Integrity check failed.' else: diff --git a/pyas2lib/tests/__init__.py b/pyas2lib/tests/__init__.py index a72a2fa..73e66d8 100644 --- a/pyas2lib/tests/__init__.py +++ b/pyas2lib/tests/__init__.py @@ -11,6 +11,7 @@ def setUpClass(cls): """Perform the setup actions for the test case.""" file_list = { 'test_data': 'payload.txt', + 'test_data_dos': 'payload_dos.txt', 'private_key': 'cert_test.p12', 'public_key': 'cert_test_public.pem', 'mecas2_public_key': 'cert_mecas2_public.pem', diff --git a/pyas2lib/tests/test_advanced.py b/pyas2lib/tests/test_advanced.py index be776af..8417051 100644 --- a/pyas2lib/tests/test_advanced.py +++ b/pyas2lib/tests/test_advanced.py @@ -410,7 +410,7 @@ def test_all_encryption_algos(self): # Compare the mic contents of the input and output messages self.assertEqual(status, 'processed') self.assertTrue(in_message.encrypted) - self.assertEqual(self.test_data, in_message.content) + self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) def find_org(self, headers): return self.org @@ -444,8 +444,7 @@ def setUp(self): def xtest_process_message(self): """ Test processing message received from Sterling Integrator""" - with open(os.path.join( - TEST_DIR, 'sb2bi_signed_cmp.msg'), 'rb') as msg: + with open(os.path.join( TEST_DIR, 'sb2bi_signed_cmp.msg'), 'rb') as msg: as2message = as2.Message() status, exception, as2mdn = as2message.parse( msg.read(), @@ -457,14 +456,12 @@ def xtest_process_message(self): def test_process_mdn(self): """ Test processing mdn received from Sterling Integrator""" - message = as2.Message(sender=self.org, receiver=self.partner) - message.message_id = '151694007918.24690.7052273208458909245@' \ + msg = as2.Message(sender=self.org, receiver=self.partner) + msg.message_id = '151694007918.24690.7052273208458909245@' \ 'ip-172-31-14-209.ec2.internal' as2mdn = as2.Mdn() # Parse the mdn and get the message status - with open(os.path.join( - TEST_DIR, 'sb2bi_signed.mdn'), 'rb') as mdn: - status, detailed_status = as2mdn.parse( - mdn.read(), lambda x, y: message) + with open(os.path.join(TEST_DIR, 'sb2bi_signed.mdn'), 'rb') as mdn: + status, detailed_status = as2mdn.parse(mdn.read(), lambda x, y: msg) self.assertEqual(status, 'processed') diff --git a/pyas2lib/tests/test_basic.py b/pyas2lib/tests/test_basic.py index 1458a04..38fc540 100644 --- a/pyas2lib/tests/test_basic.py +++ b/pyas2lib/tests/test_basic.py @@ -61,7 +61,7 @@ def test_compressed_message(self): # Compare the mic contents of the input and output messages self.assertEqual(status, 'processed') self.assertTrue(in_message.compressed) - self.assertEqual(self.test_data, in_message.content) + self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) def test_encrypted_message(self): """ Test Encrypted Unsigned Uncompressed Message """ @@ -83,7 +83,7 @@ def test_encrypted_message(self): # Compare the mic contents of the input and output messages self.assertEqual(status, 'processed') self.assertTrue(in_message.encrypted) - self.assertEqual(self.test_data, in_message.content) + self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) def test_signed_message(self): """ Test Unencrypted Signed Uncompressed Message """ @@ -104,7 +104,7 @@ def test_signed_message(self): # Compare the mic contents of the input and output messages self.assertEqual(status, 'processed') - self.assertEqual(self.test_data, in_message.content) + self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) self.assertTrue(in_message.signed) self.assertEqual(out_message.mic, in_message.mic) @@ -131,7 +131,32 @@ def test_encrypted_signed_message(self): self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertEqual(out_message.mic, in_message.mic) - self.assertEqual(self.test_data, in_message.content) + self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) + + def test_encrypted_signed_message_dos(self): + """ Test Encrypted Signed Uncompressed Message with DOS line endings. """ + + # Build an As2 message to be transmitted to partner + self.partner.sign = True + self.partner.encrypt = True + out_message = as2.Message(self.org, self.partner) + out_message.build(self.test_data_dos) + raw_out_message = out_message.headers_str + b'\r\n' + out_message.content + + # Parse the generated AS2 message as the partner + in_message = as2.Message() + status, _, _ = in_message.parse( + raw_out_message, + find_org_cb=self.find_org, + find_partner_cb=self.find_partner + ) + + # Compare the mic contents of the input and output messages + self.assertEqual(status, 'processed') + self.assertTrue(in_message.signed) + self.assertTrue(in_message.encrypted) + self.assertEqual(out_message.mic, in_message.mic) + self.assertEqual(self.test_data_dos, in_message.content) def test_encrypted_signed_compressed_message(self): """ Test Encrypted Signed Compressed Message """ @@ -154,11 +179,11 @@ def test_encrypted_signed_compressed_message(self): # Compare the mic contents of the input and output messages self.assertEqual(status, 'processed') - self.assertEqual(self.test_data, in_message.content) self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertTrue(in_message.compressed) self.assertEqual(out_message.mic, in_message.mic) + self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) def find_org(self, as2_id): return self.org diff --git a/pyas2lib/tests/test_utils.py b/pyas2lib/tests/test_utils.py index 072a528..c14a246 100644 --- a/pyas2lib/tests/test_utils.py +++ b/pyas2lib/tests/test_utils.py @@ -19,8 +19,8 @@ def test_bytes_generator(): """Test the email bytes generator class.""" message = Message() message.set_type('application/pkcs7-mime') - assert utils.mime_to_bytes(message, 0) == b'MIME-Version: 1.0\n' \ - b'Content-Type: application/pkcs7-mime\n\n' + assert utils.mime_to_bytes(message) == b'MIME-Version: 1.0\r\n' \ + b'Content-Type: application/pkcs7-mime\r\n\r\n' def test_make_boundary(): diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index 920611f..073747c 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -4,6 +4,8 @@ import sys from OpenSSL import crypto from asn1crypto import pem +from email import policy +from email import message from email.generator import BytesGenerator from io import BytesIO @@ -50,38 +52,37 @@ def _handle_application_pkcs7_mime(self, msg: email.message.Message): self._fp.write(payload) -def mime_to_bytes(msg: email.message.Message, header_len): +def mime_to_bytes(msg: message.Message, email_policy: policy.Policy = policy.HTTP): """ Function to convert and email Message to flat string format. :param msg: email.Message to be converted to string - :param header_len: the msx length of the header per line + :param email_policy: the policy to be used for flattening the message. :return: the byte string representation of the email message """ fp = BytesIO() - g = BinaryBytesGenerator(fp, maxheaderlen=header_len) + g = BinaryBytesGenerator(fp, policy=email_policy) g.flatten(msg) return fp.getvalue() -def canonicalize(message: email.message.Message): +def canonicalize(email_message: message.Message): """ Function to convert an email Message to standard format string/ - :param message: email.Message to be converted to standard string + :param email_message: email.message.Message to be converted to standard string :return: the standard representation of the email message in bytes """ - if message.get('Content-Transfer-Encoding') == 'binary': + if email_message.get('Content-Transfer-Encoding') == 'binary': message_header = '' - message_body = message.get_payload(decode=True) - for k, v in message.items(): + message_body = email_message.get_payload(decode=True) + for k, v in email_message.items(): message_header += '{}: {}\r\n'.format(k, v) message_header += '\r\n' return message_header.encode('utf-8') + message_body else: - return mime_to_bytes(message, 0).replace( - b'\r\n', b'\n').replace(b'\r', b'\n').replace(b'\n', b'\r\n') + return mime_to_bytes(email_message) def make_mime_boundary(text: str = None): From e81fd886de4ee55fb8b3d502e9da97907c36ed58 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Wed, 12 Jun 2019 10:07:47 +0530 Subject: [PATCH 32/66] use f strings for string formatting --- pyas2lib/as2.py | 135 ++++++++++++++++++++---------------------------- 1 file changed, 56 insertions(+), 79 deletions(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 6f96bc7..54d3b20 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -289,7 +289,7 @@ def headers_str(self): message_header = '' if self.payload: for k, v in self.headers.items(): - message_header += '{}: {}\r\n'.format(k, v) + message_header += f'{k}: {v}\r\n' return message_header.encode('utf-8') def build(self, data, filename=None, subject='AS2 Message', @@ -324,13 +324,11 @@ def build(self, data, filename=None, subject='AS2 Message', if self.receiver.sign and not self.sender.sign_key: raise ImproperlyConfigured( - 'Signing of messages is enabled but sign key is not set ' - 'for the sender.') + 'Signing of messages is enabled but sign key is not set for the sender.') if self.receiver.encrypt and not self.receiver.encrypt_cert: raise ImproperlyConfigured( - 'Encryption of messages is enabled but encrypt key is not set ' - 'for the receiver.') + 'Encryption of messages is enabled but encrypt key is not set for the receiver.') # Generate message id using UUID 1 as it uses both hostname and time self.message_id = email_utils.make_msgid().lstrip('<').rstrip('>') @@ -339,7 +337,7 @@ def build(self, data, filename=None, subject='AS2 Message', as2_headers = { 'AS2-Version': AS2_VERSION, 'ediint-features': EDIINT_FEATURES, - 'Message-ID': '<{}>'.format(self.message_id), + 'Message-ID': f'<{self.message_id}>', 'AS2-From': quote_as2name(self.sender.as2_name), 'AS2-To': quote_as2name(self.receiver.as2_name), 'Subject': subject, @@ -355,8 +353,7 @@ def build(self, data, filename=None, subject='AS2 Message', encoders.encode_7or8bit(self.payload) if filename: - self.payload.add_header( - 'Content-Disposition', 'attachment', filename=filename) + self.payload.add_header('Content-Disposition', 'attachment', filename=filename) del self.payload['MIME-Version'] if self.receiver.compress: @@ -365,20 +362,17 @@ def build(self, data, filename=None, subject='AS2 Message', compressed_message.set_type('application/pkcs7-mime') compressed_message.set_param('name', 'smime.p7z') compressed_message.set_param('smime-type', 'compressed-data') - compressed_message.add_header( - 'Content-Disposition', 'attachment', filename='smime.p7z') + compressed_message.add_header('Content-Disposition', 'attachment', filename='smime.p7z') compressed_message.add_header('Content-Transfer-Encoding', 'binary') compressed_message.set_payload(compress_message(mime_to_bytes(self.payload))) - self.payload = compressed_message - logger.debug('Compressed message %s payload as:\n%s' % ( - self.message_id, self.payload.as_string())) + logger.debug( + f'Compressed message {self.message_id} payload as:\n{self.payload.as_string()}') if self.receiver.sign: self.signed, self.digest_alg = True, self.receiver.digest_alg - signed_message = MIMEMultipart( - 'signed', protocol="application/pkcs7-signature") + signed_message = MIMEMultipart('signed', protocol="application/pkcs7-signature") del signed_message['MIME-Version'] signed_message.attach(self.payload) @@ -393,19 +387,18 @@ def build(self, data, filename=None, subject='AS2 Message', signature.set_type('application/pkcs7-signature') signature.set_param('name', 'smime.p7s') signature.set_param('smime-type', 'signed-data') - signature.add_header( - 'Content-Disposition', 'attachment', filename='smime.p7s') + signature.add_header('Content-Disposition', 'attachment', filename='smime.p7s') del signature['MIME-Version'] - signature.set_payload(sign_message( - mic_content, self.digest_alg, self.sender.sign_key)) + signature_data = sign_message(mic_content, self.digest_alg, self.sender.sign_key) + signature.set_payload(signature_data) encoders.encode_base64(signature) signed_message.set_param('micalg', self.digest_alg) signed_message.attach(signature) self.payload = signed_message - logger.debug('Signed message %s payload as:\n%s' % ( - self.message_id, mime_to_bytes(self.payload))) + logger.debug( + f'Signed message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}') if self.receiver.encrypt: self.encrypted, self.enc_alg = True, self.receiver.enc_alg @@ -413,29 +406,27 @@ def build(self, data, filename=None, subject='AS2 Message', encrypted_message.set_type('application/pkcs7-mime') encrypted_message.set_param('name', 'smime.p7m') encrypted_message.set_param('smime-type', 'enveloped-data') - encrypted_message.add_header( - 'Content-Disposition', 'attachment', filename='smime.p7m') + encrypted_message.add_header('Content-Disposition', 'attachment', filename='smime.p7m') encrypted_message.add_header('Content-Transfer-Encoding', 'binary') encrypt_cert = self.receiver.load_encrypt_cert() - encrypted_message.set_payload(encrypt_message( - mime_to_bytes(self.payload), self.enc_alg, encrypt_cert)) + encrypted_data = encrypt_message( + mime_to_bytes(self.payload), self.enc_alg, encrypt_cert) + encrypted_message.set_payload(encrypted_data) self.payload = encrypted_message - logger.debug('Encrypted message %s payload as:\n%s' % ( - self.message_id, self.payload.as_string())) + logger.debug( + f'Encrypted message {self.message_id} payload as:\n{self.payload.as_string()}') if self.receiver.mdn_mode: as2_headers['disposition-notification-to'] = 'no-reply@pyas2.com' if self.receiver.mdn_digest_alg: as2_headers['disposition-notification-options'] = \ - 'signed-receipt-protocol=required, pkcs7-signature; ' \ - 'signed-receipt-micalg=optional, {}'.format( - self.receiver.mdn_digest_alg) + f'signed-receipt-protocol=required, pkcs7-signature; ' \ + f'signed-receipt-micalg=optional, {self.receiver.mdn_digest_alg}' if self.receiver.mdn_mode == 'ASYNC': if not self.sender.mdn_url: raise ImproperlyConfigured( - 'MDN URL must be set in the organization when MDN mode ' - 'is set to ASYNC') + 'MDN URL must be set in the organization when MDN mode is set to ASYNC') as2_headers['receipt-delivery-option'] = self.sender.mdn_url # Update the headers of the final payload and set its boundary @@ -503,31 +494,27 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, org_id = unquote_as2name(as2_headers['as2-to']) self.receiver = find_org_cb(org_id) if not self.receiver: - raise PartnerNotFound( - 'Unknown AS2 organization with id {}'.format(org_id)) + raise PartnerNotFound(f'Unknown AS2 organization with id {org_id}') partner_id = unquote_as2name(as2_headers['as2-from']) self.sender = find_partner_cb(partner_id) if not self.sender: - raise PartnerNotFound( - 'Unknown AS2 partner with id {}'.format(partner_id)) + raise PartnerNotFound(f'Unknown AS2 partner with id {partner_id}') - if find_message_cb and \ - find_message_cb(self.message_id, partner_id): + if find_message_cb and find_message_cb(self.message_id, partner_id): raise DuplicateDocument( - 'Duplicate message received, message with this ID ' - 'already processed.') + 'Duplicate message received, message with this ID already processed.') if self.sender.encrypt and \ self.payload.get_content_type() != 'application/pkcs7-mime': raise InsufficientSecurityError( - 'Incoming messages from partner {} are must be encrypted' - ' but encrypted message not found.'.format(partner_id)) + f'Incoming messages from partner {partner_id} are must be encrypted ' + f'but encrypted message not found.') if self.payload.get_content_type() == 'application/pkcs7-mime' \ and self.payload.get_param('smime-type') == 'enveloped-data': - logger.debug('Decrypting message %s payload :\n%s' % ( - self.message_id, self.payload.as_string())) + logger.debug( + f'Decrypting message {self.message_id} payload :\n{self.payload.as_string()}') self.encrypted = True encrypted_data = self.payload.get_payload(decode=True) @@ -544,21 +531,20 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, # Check for compressed data here self.compressed, self.payload = self._decompress_data(self.payload) - if self.sender.sign and \ - self.payload.get_content_type() != 'multipart/signed': + if self.sender.sign and self.payload.get_content_type() != 'multipart/signed': raise InsufficientSecurityError( - 'Incoming messages from partner {} are must be signed ' - 'but signed message not found.'.format(partner_id)) + f'Incoming messages from partner {partner_id} are must be signed ' + f'but signed message not found.') if self.payload.get_content_type() == 'multipart/signed': - logger.debug('Verifying signed message %s payload: \n%s' % ( - self.message_id, self.payload.as_string())) + logger.debug( + f'Verifying signed message {self.message_id} ' + f'payload: \n{self.payload.as_string()}') self.signed = True # Split the message into signature and signed message signature = None - signature_types = ['application/pkcs7-signature', - 'application/x-pkcs7-signature'] + signature_types = ['application/pkcs7-signature', 'application/x-pkcs7-signature'] for part in self.payload.walk(): if part.get_content_type() in signature_types: signature = part.get_payload(decode=True) @@ -583,11 +569,9 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, except Exception as e: status = getattr(e, 'disposition_type', 'processed/Error') - detailed_status = getattr( - e, 'disposition_modifier', 'unexpected-processing-error') + detailed_status = getattr(e, 'disposition_modifier', 'unexpected-processing-error') exception = (e, traceback.format_exc()) - logger.error('Failed to parse AS2 message\n: %s' % - traceback.format_exc()) + logger.error(f'Failed to parse AS2 message\n: {traceback.format_exc()}') finally: # Update the payload headers with the original headers @@ -605,12 +589,9 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, digest_alg = as2_headers.get('disposition-notification-options') if digest_alg: - digest_alg = digest_alg.split(';')[-1].\ - split(',')[-1].strip() - mdn = Mdn( - mdn_mode=mdn_mode, mdn_url=mdn_url, digest_alg=digest_alg) - mdn.build( - message=self, status=status, detailed_status=detailed_status) + digest_alg = digest_alg.split(';')[-1].split(',')[-1].strip() + mdn = Mdn(mdn_mode=mdn_mode, mdn_url=mdn_url, digest_alg=digest_alg) + mdn.build(message=self, status=status, detailed_status=detailed_status) return status, exception, mdn @@ -653,7 +634,7 @@ def headers_str(self): message_header = '' if self.payload: for k, v in self.headers.items(): - message_header += '{}: {}\r\n'.format(k, v) + message_header += f'{k}: {v}\r\n' return message_header.encode('utf-8') def build(self, message, status, detailed_status=None, confirmation_text=MDN_CONFIRM_TEXT, @@ -680,7 +661,7 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON mdn_headers = { 'AS2-Version': AS2_VERSION, 'ediint-features': EDIINT_FEATURES, - 'Message-ID': '<{}>'.format(self.message_id), + 'Message-ID': f'<{self.message_id}>', 'AS2-From': quote_as2name(message.headers.get('as2-to')), 'AS2-To': quote_as2name(message.headers.get('as2-from')), 'Date': email_utils.formatdate(localtime=True), @@ -703,7 +684,7 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON # Create and attach the MDN Text Message mdn_text = email_message.Message() - mdn_text.set_payload('%s\r\n' % confirmation_text) + mdn_text.set_payload(f'{confirmation_text}\r\n') mdn_text.set_type('text/plain') del mdn_text['MIME-Version'] encoders.encode_7or8bit(mdn_text) @@ -713,26 +694,22 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON mdn_base = email_message.Message() mdn_base.set_type('message/disposition-notification') mdn_report = 'Reporting-UA: pyAS2 Open Source AS2 Software\r\n' - mdn_report += 'Original-Recipient: rfc822; {}\r\n'.format( - message.headers.get('as2-to')) - mdn_report += 'Final-Recipient: rfc822; {}\r\n'.format( - message.headers.get('as2-to')) - mdn_report += 'Original-Message-ID: <{}>\r\n'.format(message.message_id) - mdn_report += 'Disposition: automatic-action/' \ - 'MDN-sent-automatically; {}'.format(status) + mdn_report += f'Original-Recipient: rfc822; {message.headers.get("as2-to")}\r\n' + mdn_report += f'Final-Recipient: rfc822; {message.headers.get("as2-to")}\r\n' + mdn_report += f'Original-Message-ID: <{message.message_id}>\r\n' + mdn_report += f'Disposition: automatic-action/MDN-sent-automatically; {status}' if detailed_status: - mdn_report += ': {}'.format(detailed_status) + mdn_report += f': {detailed_status}' mdn_report += '\r\n' if message.mic: - mdn_report += 'Received-content-MIC: {}, {}\r\n'.format( - message.mic.decode(), message.digest_alg) + mdn_report += f'Received-content-MIC: {message.mic.decode()}, {message.digest_alg}\r\n' mdn_base.set_payload(mdn_report) del mdn_base['MIME-Version'] encoders.encode_7or8bit(mdn_base) self.payload.attach(mdn_base) - logger.debug('MDN for message %s created:\n%s' % ( - message.message_id, mdn_base.as_string())) + logger.debug( + f'MDN for message {message.message_id} created:\n{mdn_base.as_string()}') # Sign the MDN if it is requested by the sender if message.headers.get('disposition-notification-options') and \ @@ -762,8 +739,8 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON signed_mdn.attach(signature) self.payload = signed_mdn - logger.debug(f'Signature for MDN %s created:\n%s' % ( - message.message_id, signature.as_string())) + logger.debug( + f'Signature for MDN {message.message_id} created:\n{signature.as_string()}') # Update the headers of the final payload and set message boundary for k, v in mdn_headers.items(): From b9ccf14bce1160d7afcd4a5337fe4b7556575fa2 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Wed, 12 Jun 2019 13:37:30 +0530 Subject: [PATCH 33/66] remove the option for cms encoding --- pyas2lib/as2.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 54d3b20..a81dee1 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -143,10 +143,6 @@ class Partner(object): :param enc_alg: The encryption algorithm to be used. (default `"tripledes_192_cbc"`) - :param cms_encoding: - The encoding to be used for the encrypted, signed or compressed data. - It can be `'binary'` or `'base64'`. (default `'binary'`) - :param mdn_mode: The mode to be used for receiving the MDN. Set to `None` for no MDN, `'SYNC'` for synchronous and `'ASYNC'` for asynchronous. (default `None`) @@ -170,7 +166,6 @@ class Partner(object): enc_alg: str = 'tripledes_192_cbc' sign: bool = False digest_alg: str = 'sha256' - cms_encoding: str = 'binary' mdn_mode: str = None mdn_digest_alg: str = None mdn_confirm_text: str = MDN_CONFIRM_TEXT From fbafaf07857e007d6abd43966b06cbfe3afd6d7f Mon Sep 17 00:00:00 2001 From: abhishekram Date: Wed, 12 Jun 2019 15:48:32 +0530 Subject: [PATCH 34/66] add the basic usage documentation --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fd0e233..990cd01 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,85 @@ A pure python library for building and parsing message as part of the AS2 messag * Parsing a received MIME data and identifying if it as a Message or MDN. * Decompress, Decrypt and Verify Signature of the received payload. * Verify Signature of the received MDN and extract original message status. - + + +## Basic Usage + +Let us take a look at how we can use this library for building and parsing of AS2 Messages. + +### Setup + +* First we would need to setup an organization and a partner +```python +from pyas2lib.as2 import Organization, Partner + +my_org = Organization( + as2_name='my_unique_id', # Unique AS2 Id for this organization + sign_key=b'signature_key_bytes', # PEM/DER encoded private key for signature + sign_key_pass='password', # Password private key for signature + decrypt_key=b'decrypt_key_bytes', # PEM/DER encoded private key for decryption + decrypt_key_pass='password' # Password private key for decryption +) + +a_partner = Partner( + as2_name='partner_unique_id', # Unique AS2 Id of your partner + sign=True, # Set to true for signing the message + verify_cert=b'verify_cert_bytes', # PEM/DER encoded certificate for verifying partner signatures + encrypt=True, # Set to true for encrypting the message + encrypt_cert=b'encrypt_cert_bytes', # PEM/DER encoded certificate for encrypting messages + mdn_mode='SYNC', # Expect to receive synchronous MDNs from this partner + mdn_digest_alg='sha256' # Expect signed MDNs to be returned by this partner +) + +``` + +### Sending a message to your partner + +* The partner is now setup we can build and AS2 message +```python +from pyas2lib.as2 import Message + +msg = Message(sender=my_org, receiver=a_partner) +msg.build(b'data_to_transmit') + +``` +* The message is built and now `msg.content` holds the message body and `message.header` dictionary holds the message headers. These need to be passed to any http library for HTTP POSTing to the partner. +* We expect synchronous MDNs so we need to process the response to our HTTP POST +```python +from pyas2lib.as2 import Mdn + +msg_mdn = Mdn() # Initialize an Mdn object + +# Call the parse method with the HTTP response headers + content and a function that returns the related `pyas2lib.as2.Messsage` object. +status, detailed_status = msg_mdn.parse(b'response_data_with_headers', find_message_func) +``` +* We parse the response mdn to get the status and detailed status of the message that was transmitted. + +### Receiving a message from your partner + +* We need to setup and HTTP server with an endpoint for receiving POST requests fro your partner. +* When a requests is received we need to first check if this is an Async MDN +```python +from pyas2lib.as2 import Mdn + +msg_mdn = Mdn() # Initialize an Mdn object +# Call the parse method with the HTTP request headers + content and a function the returns the related `pyas2lib.as2.Messsage` object. +status, detailed_status = msg_mdn.parse(request_body, find_message_fumc) +``` +* If this is an Async MDN it will return the status of the original message. +* In case the request is not an MDN then `pyas2lib.exceptions.MDNNotFound` is raised, which needs to be catched and parse the request as a message. +```python +from pyas2lib.as2 import Message + +msg = Message() +# Call the parse method with the HTTP request headers + content, a function to return the the related `pyas2lib.as2.Organization` object, a function to return the `pyas2lib.as2.Partner` object and a function to check for duplicates. +status, exception, mdn = msg.parse( + request_body, find_organization, find_partner, check_duplicate_msg) +``` +* The parse function returns a 3 element tuple; the status of parsing, exception if any raised during parsing and an `pyas2lib.as2.Mdn` object for the message. +* If the `mdn.mdn_mode` is `SYNC` then the `mdn.content` and `mdn.header` must be returned in the response. +* If the `mdn.mdn_mode` is `ASYNC` then the mdn must be saved for later processing. + ## Contribute 1. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. From 58f5523ccf96c4b9201d382e2997216c0f3cef55 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Wed, 12 Jun 2019 15:59:07 +0530 Subject: [PATCH 35/66] Bump version to 1.2.0 --- CHANGELOG.md | 10 ++++++++++ pyas2lib/__init__.py | 12 +++++++----- setup.py | 3 +-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9781838..b80993f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 1.2.0 - 2019-06-12 + +* Use f-strings for string formatting. +* Use HTTP email policy for flattening email messages. +* Add proper support for other encryption algos. +* Use dataclasses for organization and partner. +* Remove support for python 3.5. +* Add utility function for extracting info from certificates. + + ## 1.1.1 - 2019-06-03 * Remove leftover print statement. diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index eefd495..1cc0a5e 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -1,13 +1,15 @@ -from pyas2lib.as2 import DIGEST_ALGORITHMS -from pyas2lib.as2 import ENCRYPTION_ALGORITHMS -from pyas2lib.as2 import MDN_CONFIRM_TEXT -from pyas2lib.as2 import MDN_FAILED_TEXT +from pyas2lib.constants import ( + DIGEST_ALGORITHMS, + ENCRYPTION_ALGORITHMS, + MDN_CONFIRM_TEXT, + MDN_FAILED_TEXT +) from pyas2lib.as2 import Mdn from pyas2lib.as2 import Message from pyas2lib.as2 import Organization from pyas2lib.as2 import Partner -__version__ = '1.1.1' +__version__ = '1.2.0' __all__ = [ diff --git a/setup.py b/setup.py index e58394b..bb77d41 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,6 @@ 'pytest==3.4.0', 'pytest-cov==2.5.1', 'coverage==4.3.4', - 'nose', ] setup( @@ -27,7 +26,7 @@ long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version='1.1.1', + version='1.2.0', author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages( From 215d40d96a7608c886216d0acc4312499373ea6c Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 25 Jun 2019 15:50:08 +0530 Subject: [PATCH 36/66] handle exceptions with parsing signed attributes and more debug logging --- pyas2lib/as2.py | 154 +++++++++++++++++++------------------ pyas2lib/cms.py | 14 ++-- pyas2lib/tests/test_mdn.py | 32 ++++++++ pyas2lib/utils.py | 16 ++-- 4 files changed, 132 insertions(+), 84 deletions(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 8173f5d..7042dda 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -368,7 +368,7 @@ def build(self, data, filename=None, subject='AS2 Message', self.payload = compressed_message logger.debug( - f'Compressed message {self.message_id} payload as:\n{self.payload.as_string()}') + f'Compressed message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}') if self.receiver.sign: self.signed, self.digest_alg = True, self.receiver.digest_alg @@ -415,7 +415,7 @@ def build(self, data, filename=None, subject='AS2 Message', self.payload = encrypted_message logger.debug( - f'Encrypted message {self.message_id} payload as:\n{self.payload.as_string()}') + f'Encrypted message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}') if self.receiver.mdn_mode: as2_headers['disposition-notification-to'] = disposition_notification_to @@ -439,10 +439,11 @@ def build(self, data, filename=None, subject='AS2 Message', if self.payload.is_multipart(): self.payload.set_boundary(make_mime_boundary()) - @staticmethod - def _decompress_data(payload): + def _decompress_data(self, payload): if payload.get_content_type() == 'application/pkcs7-mime' \ and payload.get_param('smime-type') == 'compressed-data': + logger.debug(f'Decompressing message {self.message_id} payload :\n' + f'{mime_to_bytes(self.payload)}') compressed_data = payload.get_payload(decode=True) decompressed_data = decompress_message(compressed_data) return True, parse_mime(decompressed_data) @@ -513,8 +514,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, if self.payload.get_content_type() == 'application/pkcs7-mime' \ and self.payload.get_param('smime-type') == 'enveloped-data': - logger.debug( - f'Decrypting message {self.message_id} payload :\n{self.payload.as_string()}') + logger.debug(f'Decrypting message {self.message_id} payload :\n' + f'{mime_to_bytes(self.payload)}') self.encrypted = True encrypted_data = self.payload.get_payload(decode=True) @@ -537,9 +538,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, f'but signed message not found.') if self.payload.get_content_type() == 'multipart/signed': - logger.debug( - f'Verifying signed message {self.message_id} ' - f'payload: \n{self.payload.as_string()}') + logger.debug(f'Verifying signed message {self.message_id} payload: \n' + f'{mime_to_bytes(self.payload)}') self.signed = True # Split the message into signature and signed message @@ -555,8 +555,7 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, # then convert to canonical form and try again mic_content = canonicalize(self.payload) verify_cert = self.sender.load_verify_cert() - self.digest_alg = verify_message( - mic_content, signature, verify_cert) + self.digest_alg = verify_message(mic_content, signature, verify_cert) # Calculate the MIC Hash of the message to be verified digest_func = hashlib.new(self.digest_alg) @@ -590,6 +589,9 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, digest_alg = as2_headers.get('disposition-notification-options') if digest_alg: digest_alg = digest_alg.split(';')[-1].split(',')[-1].strip() + + logger.debug(f'Building the MDN for message {self.message_id} with status {status} ' + f'and detailed-status {detailed_status}.') mdn = Mdn(mdn_mode=mdn_mode, mdn_url=mdn_url, digest_alg=digest_alg) mdn.build(message=self, status=status, detailed_status=detailed_status) @@ -709,14 +711,13 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON self.payload.attach(mdn_base) logger.debug( - f'MDN for message {message.message_id} created:\n{mdn_base.as_string()}') + f'MDN report for message {message.message_id} created:\n{mime_to_bytes(mdn_base)}') # Sign the MDN if it is requested by the sender if message.headers.get('disposition-notification-options') and \ message.receiver and message.receiver.sign_key: - self.digest_alg = \ - message.headers['disposition-notification-options'].\ - split(';')[-1].split(',')[-1].strip().replace('-', '') + self.digest_alg = message.headers['disposition-notification-options'].\ + split(';')[-1].split(',')[-1].strip().replace('-', '') signed_mdn = MIMEMultipart('signed', protocol="application/pkcs7-signature") del signed_mdn['MIME-Version'] signed_mdn.attach(self.payload) @@ -739,8 +740,7 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON signed_mdn.attach(signature) self.payload = signed_mdn - logger.debug( - f'Signature for MDN {message.message_id} created:\n{signature.as_string()}') + logger.debug(f'Signing the MDN for message {message.message_id}') # Update the headers of the final payload and set message boundary for k, v in mdn_headers.items(): @@ -749,6 +749,8 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON else: self.payload.add_header(k, v) self.payload.set_boundary(make_mime_boundary()) + logger.debug(f'MDN generated for message {message.message_id} with ' + f'content:\n {mime_to_bytes(self.payload)}') def parse(self, raw_content, find_message_cb): """Function parses the RAW AS2 MDN, verifies it and extracts the @@ -770,68 +772,74 @@ def parse(self, raw_content, find_message_cb): """ status, detailed_status = None, None - self.payload = parse_mime(raw_content) - self.orig_message_id, orig_recipient = self.detect_mdn() - - # Call the find message callback which should return a Message instance - orig_message = find_message_cb(self.orig_message_id, orig_recipient) + try: + self.payload = parse_mime(raw_content) + self.orig_message_id, orig_recipient = self.detect_mdn() + + # Call the find message callback which should return a Message instance + orig_message = find_message_cb(self.orig_message_id, orig_recipient) + + # Extract the headers and save it + mdn_headers = {} + for k, v in self.payload.items(): + k = k.lower() + if k == 'message-id': + self.message_id = v.lstrip('<').rstrip('>') + mdn_headers[k] = v + + if orig_message.receiver.mdn_digest_alg \ + and self.payload.get_content_type() != 'multipart/signed': + status = 'failed/Failure' + detailed_status = 'Expected signed MDN but unsigned MDN returned' + return status, detailed_status - # Extract the headers and save it - mdn_headers = {} - for k, v in self.payload.items(): - k = k.lower() - if k == 'message-id': - self.message_id = v.lstrip('<').rstrip('>') - mdn_headers[k] = v + if self.payload.get_content_type() == 'multipart/signed': + logger.debug(f'Verifying signed MDN: \n{mime_to_bytes(self.payload)}') + message_boundary = ('--' + self.payload.get_boundary()).encode('utf-8') - if orig_message.receiver.mdn_digest_alg \ - and self.payload.get_content_type() != 'multipart/signed': - status = 'failed/Failure' - detailed_status = 'Expected signed MDN but unsigned MDN returned' - return status, detailed_status + # Extract the signature and the signed payload + signature = None + signature_types = ['application/pkcs7-signature', 'application/x-pkcs7-signature'] + for part in self.payload.walk(): + if part.get_content_type() in signature_types: + signature = part.get_payload(decode=True) + elif part.get_content_type() == 'multipart/report': + self.payload = part - if self.payload.get_content_type() == 'multipart/signed': - message_boundary = ('--' + self.payload.get_boundary()).encode('utf-8') + # Verify the message, first using raw message and if it fails + # then convert to canonical form and try again + mic_content = extract_first_part(raw_content, message_boundary) + verify_cert = orig_message.receiver.load_verify_cert() + try: + self.digest_alg = verify_message(mic_content, signature, verify_cert) + except IntegrityError: + mic_content = canonicalize(self.payload) + self.digest_alg = verify_message(mic_content, signature, verify_cert) - # Extract the signature and the signed payload - signature = None - signature_types = ['application/pkcs7-signature', - 'application/x-pkcs7-signature'] for part in self.payload.walk(): - if part.get_content_type() in signature_types: - signature = part.get_payload(decode=True) - elif part.get_content_type() == 'multipart/report': - self.payload = part - - # Verify the message, first using raw message and if it fails - # then convert to canonical form and try again - mic_content = extract_first_part(raw_content, message_boundary) - verify_cert = orig_message.receiver.load_verify_cert() - try: - self.digest_alg = verify_message(mic_content, signature, verify_cert) - except IntegrityError: - mic_content = canonicalize(self.payload) - self.digest_alg = verify_message(mic_content, signature, verify_cert) + if part.get_content_type() == 'message/disposition-notification': + logger.debug( + f'MDN report for message {orig_message.message_id}:\n{part.as_string()}') + + mdn = part.get_payload()[-1] + mdn_status = mdn['Disposition'].split(';').pop().strip().split(':') + status = mdn_status[0] + if status == 'processed': + mdn_mic = mdn.get('Received-Content-MIC', '').split(',')[0] + + # TODO: Check MIC for all cases + if mdn_mic and orig_message.mic and mdn_mic != orig_message.mic.decode(): + status = 'processed/warning' + detailed_status = 'Message Integrity check failed.' + else: + detailed_status = ' '.join(mdn_status[1:]).strip() + except Exception as e: + status = 'failed/Failure' + detailed_status = f'Failed to parse received MDN. {e}' + logger.error(f'Failed to parse AS2 MDN\n: {traceback.format_exc()}') - for part in self.payload.walk(): - if part.get_content_type() == 'message/disposition-notification': - logger.debug( - f'Found MDN report for message {orig_message.message_id}:\n{part.as_string()}') - - mdn = part.get_payload()[-1] - mdn_status = mdn['Disposition'].split(';').pop().strip().split(':') - status = mdn_status[0] - if status == 'processed': - mdn_mic = mdn.get('Received-Content-MIC', '').split(',')[0] - - # TODO: Check MIC for all cases - if mdn_mic and orig_message.mic and mdn_mic != orig_message.mic.decode(): - status = 'processed/warning' - detailed_status = 'Message Integrity check failed.' - else: - detailed_status = ' '.join(mdn_status[1:]).strip() - - return status, detailed_status + finally: + return status, detailed_status def detect_mdn(self): """ Function checks if the received raw message is an AS2 MDN or not. diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 0c37763..72bb141 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -343,11 +343,10 @@ def verify_message(data_to_verify, signature, verify_cert): cms_content = cms.ContentInfo.load(signature) digest_alg = None if cms_content['content_type'].native == 'signed_data': + for signer in cms_content['content']['signer_infos']: - signed_attributes = signer['signed_attrs'].copy() digest_alg = signer['digest_algorithm']['algorithm'].native - if digest_alg not in DIGEST_ALGORITHMS: raise Exception('Unsupported Digest Algorithm') @@ -355,10 +354,13 @@ def verify_message(data_to_verify, signature, verify_cert): sig = signer['signature'].native signed_data = data_to_verify - if signed_attributes: + if signer['signed_attrs']: attr_dict = {} - for attr in signed_attributes.native: - attr_dict[attr['type']] = attr['values'] + for attr in signer['signed_attrs']: + try: + attr_dict[attr.native['type']] = attr.native['values'] + except (ValueError, KeyError): + continue message_digest = bytes() for d in attr_dict['message_digest']: @@ -371,7 +373,7 @@ def verify_message(data_to_verify, signature, verify_cert): raise IntegrityError( 'Failed to verify message signature: Message Digest does not match.') - signed_data = signed_attributes.untag().dump() + signed_data = signer['signed_attrs'].untag().dump() try: if sig_alg == 'rsassa_pkcs1v15': diff --git a/pyas2lib/tests/test_mdn.py b/pyas2lib/tests/test_mdn.py index 49f0f7a..9787841 100644 --- a/pyas2lib/tests/test_mdn.py +++ b/pyas2lib/tests/test_mdn.py @@ -77,6 +77,38 @@ def test_signed_mdn(self): ) self.assertEqual(status, 'processed') + def test_failed_mdn_parse(self): + """Test mdn parsing failures are captured.""" + # Build an As2 message to be transmitted to partner + self.partner.sign = True + self.partner.encrypt = True + self.partner.mdn_mode = as2.SYNCHRONOUS_MDN + self.partner.mdn_digest_alg = 'sha256' + self.out_message = as2.Message(self.org, self.partner) + self.out_message.build(self.test_data) + + # Parse the generated AS2 message as the partner + raw_out_message = \ + self.out_message.headers_str + b'\r\n' + self.out_message.content + in_message = as2.Message() + _, _, mdn = in_message.parse( + raw_out_message, + find_org_cb=self.find_org, + find_partner_cb=self.find_partner + ) + + out_mdn = as2.Mdn() + self.partner.verify_cert = self.mecas2_public_key + self.partner.validate_certs = False + status, detailed_status = out_mdn.parse( + mdn.headers_str + b'\r\n' + mdn.content, + find_message_cb=self.find_message + ) + self.assertEqual(status, 'failed/Failure') + self.assertEqual( + detailed_status, 'Failed to parse received MDN. Failed to verify message signature: ' + 'Message Digest does not match.') + def find_org(self, as2_id): return self.org diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index 72b33f0..94846e6 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -40,16 +40,22 @@ def quote_as2name(unquoted_name: str): class BinaryBytesGenerator(BytesGenerator): """Override the bytes generator to better handle binary data.""" - def _handle_application_pkcs7_mime(self, msg: email.message.Message): + def _handle_text(self, msg): """ Handle writing the binary messages to prevent default behaviour of newline replacements. """ - payload = msg.get_payload(decode=True) - if payload is None: - return + if msg.get('Content-Transfer-Encoding') == 'binary' and \ + msg.get_content_subtype() in ['pkcs7-mime', 'pkcs7-signature']: + payload = msg.get_payload(decode=True) + if payload is None: + return + else: + self._fp.write(payload) else: - self._fp.write(payload) + super()._handle_text(msg) + + _writeBody = _handle_text def mime_to_bytes(msg: message.Message, email_policy: policy.Policy = policy.HTTP): From 7ce672685e6cc9c3ca7875ae4ea7dae21b7ed8e0 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 25 Jun 2019 15:54:46 +0530 Subject: [PATCH 37/66] Bump version of the repository --- CHANGELOG.md | 6 +++++- pyas2lib/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b80993f..91a2fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 1.2.1 - 2019-06-25 +* Handle exceptions raised when parsing signed attributes in a signature https://github.com/abhishek-ram/django-pyas2/issues/13 +* Add more debug logs during build and parse +* Catch errors in MDN parsing and handle accordingly + ## 1.2.0 - 2019-06-12 * Use f-strings for string formatting. @@ -9,7 +14,6 @@ * Remove support for python 3.5. * Add utility function for extracting info from certificates. - ## 1.1.1 - 2019-06-03 * Remove leftover print statement. diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 1cc0a5e..84b83ac 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -9,7 +9,7 @@ from pyas2lib.as2 import Organization from pyas2lib.as2 import Partner -__version__ = '1.2.0' +__version__ = '1.2.1' __all__ = [ diff --git a/setup.py b/setup.py index bb77d41..845a627 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version='1.2.0', + version='1.2.1', author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages( From 72907fe941314510e7f3fac490d17d86c9d0d97a Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 25 Jun 2019 17:39:04 +0530 Subject: [PATCH 38/66] Handle MDNNotfound correctly when parsing an mdn --- CHANGELOG.md | 3 +++ pyas2lib/__init__.py | 2 +- pyas2lib/as2.py | 6 ++++-- setup.py | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a2fd7..8ce13a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Release History +## 1.2.2 - 2019-06-26 +* Handle MDNNotfound correctly when parsing an mdn + ## 1.2.1 - 2019-06-25 * Handle exceptions raised when parsing signed attributes in a signature https://github.com/abhishek-ram/django-pyas2/issues/13 * Add more debug logs during build and parse diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 84b83ac..726c472 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -9,7 +9,7 @@ from pyas2lib.as2 import Organization from pyas2lib.as2 import Partner -__version__ = '1.2.1' +__version__ = '1.2.2' __all__ = [ diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 7042dda..365eb19 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -825,14 +825,16 @@ def parse(self, raw_content, find_message_cb): mdn_status = mdn['Disposition'].split(';').pop().strip().split(':') status = mdn_status[0] if status == 'processed': + # Compare the original mic with the received mic mdn_mic = mdn.get('Received-Content-MIC', '').split(',')[0] - - # TODO: Check MIC for all cases if mdn_mic and orig_message.mic and mdn_mic != orig_message.mic.decode(): status = 'processed/warning' detailed_status = 'Message Integrity check failed.' else: detailed_status = ' '.join(mdn_status[1:]).strip() + except MDNNotFound: + status = 'failed/Failure' + detailed_status = 'mdn-not-found' except Exception as e: status = 'failed/Failure' detailed_status = f'Failed to parse received MDN. {e}' diff --git a/setup.py b/setup.py index 845a627..7c1fd87 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version='1.2.1', + version='1.2.2s', author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages( From 50dcec0cb8adc3bab6ffa2da0168841be31301f8 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Tue, 25 Jun 2019 17:40:00 +0530 Subject: [PATCH 39/66] Handle MDNNotfound correctly when parsing an mdn --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7c1fd87..4786850 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version='1.2.2s', + version='1.2.2', author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages( From e0789e6a77f2b5989464f7d58889406594b0eacf Mon Sep 17 00:00:00 2001 From: Fabian Brosig Date: Tue, 31 Mar 2020 19:20:46 +0200 Subject: [PATCH 40/66] Update versions of crypto dependencies The changes in asn1crypto required minor changes. Responding to the following changes in asn1crpyto's changelog: - cms.KeyEncryptionAlgorithmId().native now returns the value "rsaes_pkcs1v15" for OID 1.2.840.113549.1.1.1 instead of "rsa" - Added RC4 (1.2.840.113549.3.4) to algos.EncryptionAlgorithmId() - Require timezone-aware datetime --- pyas2lib/cms.py | 8 ++++---- setup.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 72bb141..676e99d 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -2,7 +2,7 @@ import zlib from asn1crypto import cms, core, algos from collections import OrderedDict -from datetime import datetime +from datetime import datetime, timezone from oscrypto import asymmetric, symmetric, util from pyas2lib.exceptions import * @@ -163,7 +163,7 @@ def decrypt_message(encrypted_data, decryption_key): 'key_encryption_algorithm']['algorithm'].native encrypted_key = recipient_info['encrypted_key'].native - if key_enc_alg == 'rsa': + if cms.KeyEncryptionAlgorithmId(key_enc_alg) == cms.KeyEncryptionAlgorithmId('rsa'): try: key = asymmetric.rsa_pkcs1v15_decrypt(decryption_key[0], encrypted_key) except Exception: @@ -175,7 +175,7 @@ def decrypt_message(encrypted_data, decryption_key): 'encrypted_content_info']['encrypted_content'].native try: - if alg['algorithm'].native == '1.2.840.113549.3.4': # This is RC4 + if alg['algorithm'].native == 'rc4': decrypted_content = symmetric.rc4_decrypt(key, encapsulated_data) elif alg.encryption_cipher == 'tripledes': cipher = 'tripledes_192_cbc' @@ -261,7 +261,7 @@ class SmimeCapabilities(core.Sequence): 'type': cms.CMSAttributeType('signing_time'), 'values': cms.SetOfTime([ cms.Time({ - 'utc_time': core.UTCTime(datetime.now()) + 'utc_time': core.UTCTime(datetime.utcnow().replace(tzinfo=timezone.utc)) }) ]) }), diff --git a/setup.py b/setup.py index 4786850..89b398d 100644 --- a/setup.py +++ b/setup.py @@ -2,20 +2,20 @@ from setuptools import setup, find_packages install_requires = [ - 'asn1crypto==0.24.0', - 'oscrypto==0.19.1', - 'pyOpenSSL==17.5.0', + 'asn1crypto==1.3.0', + 'oscrypto==1.2.0', + 'pyOpenSSL==19.1.0', ] if sys.version_info.minor == 6: install_requires += [ - 'dataclasses==0.6' + 'dataclasses==0.7' ] tests_require = [ - 'pytest==3.4.0', - 'pytest-cov==2.5.1', - 'coverage==4.3.4', + 'pytest==5.4.1', + 'pytest-cov==2.8.1', + 'coverage==5.0.4', ] setup( From 6cc65c828ec0f589203fb3f60b357be699995417 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sat, 4 Apr 2020 13:03:01 +0530 Subject: [PATCH 41/66] add support for python 3.8 --- .travis.yml | 1 + setup.cfg | 2 ++ setup.py | 4 ++-- tox.ini | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 setup.cfg diff --git a/.travis.yml b/.travis.yml index 90461ee..fc95967 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: python python: - '3.6' - '3.7' + - '3.8' install: - python setup.py install - pip install pytest-cov diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..9af7e6f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest \ No newline at end of file diff --git a/setup.py b/setup.py index 89b398d..ce30e78 100644 --- a/setup.py +++ b/setup.py @@ -41,12 +41,12 @@ "Programming Language :: Python", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Topic :: Security :: Cryptography", "Topic :: Communications", ], + setup_requires=['pytest-runner'], install_requires=install_requires, - - test_suite='nose.collector', tests_require=tests_require, ) diff --git a/tox.ini b/tox.ini index a4875d6..3e6822f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py35, py36, py37 +envlist = py36, py37, py38 [testenv] commands = {envpython} setup.py test From a98e2bf103be21a22da78355aaf3e30ce6c35003 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sat, 4 Apr 2020 09:37:39 +0530 Subject: [PATCH 42/66] update the smime capabilities --- pyas2lib/cms.py | 8 ++++++-- pyas2lib/tests/test_advanced.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 676e99d..6047349 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -241,11 +241,15 @@ class SmimeCapabilities(core.Sequence): smime_cap = OrderedDict([ ('0', OrderedDict([ - ('0', core.ObjectIdentifier('1.2.840.113549.3.7'))])), + ('0', core.ObjectIdentifier('2.16.840.1.101.3.4.1.42'))])), ('1', OrderedDict([ + ('0', core.ObjectIdentifier('2.16.840.1.101.3.4.1.2'))])), + ('2', OrderedDict([ + ('0', core.ObjectIdentifier('1.2.840.113549.3.7'))])), + ('3', OrderedDict([ ('0', core.ObjectIdentifier('1.2.840.113549.3.2')), ('1', core.Integer(128))])), - ('2', OrderedDict([ + ('4', OrderedDict([ ('0', core.ObjectIdentifier('1.2.840.113549.3.4')), ('1', core.Integer(128))])), ]) diff --git a/pyas2lib/tests/test_advanced.py b/pyas2lib/tests/test_advanced.py index fd15f24..f8e6d4a 100644 --- a/pyas2lib/tests/test_advanced.py +++ b/pyas2lib/tests/test_advanced.py @@ -444,7 +444,7 @@ def setUp(self): def xtest_process_message(self): """ Test processing message received from Sterling Integrator""" - with open(os.path.join( TEST_DIR, 'sb2bi_signed_cmp.msg'), 'rb') as msg: + with open(os.path.join(TEST_DIR, 'sb2bi_signed_cmp.msg'), 'rb') as msg: as2message = as2.Message() status, exception, as2mdn = as2message.parse( msg.read(), From dd363c6a5565bb981cb4c94200a89c4b2062eb27 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sat, 4 Apr 2020 19:16:13 +0530 Subject: [PATCH 43/66] increase the test coverage --- pyas2lib/cms.py | 32 ++++++++-- pyas2lib/tests/test_advanced.py | 100 ++++++++++++++++++++++++-------- pyas2lib/tests/test_cms.py | 47 +++++++++++++++ 3 files changed, 149 insertions(+), 30 deletions(-) diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 6047349..9df5f5f 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -1,8 +1,9 @@ import hashlib import zlib -from asn1crypto import cms, core, algos from collections import OrderedDict from datetime import datetime, timezone + +from asn1crypto import cms, core, algos from oscrypto import asymmetric, symmetric, util from pyas2lib.exceptions import * @@ -68,7 +69,6 @@ def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): enc_alg_list = enc_alg.split('_') cipher, key_length, mode = enc_alg_list[0], enc_alg_list[1], enc_alg_list[2] - enc_alg_asn1, encrypted_content = None, None # Generate the symmetric encryption key and encrypt the message key = util.rand_bytes(int(key_length) // 8) @@ -108,6 +108,15 @@ def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): 'algorithm': algorithm_id, 'parameters': cms.OctetString(iv) }) + elif cipher == 'des': + algorithm_id = '1.3.14.3.2.7' + iv, encrypted_content = symmetric.des_cbc_pkcs5_encrypt(key, data_to_encrypt, None) + enc_alg_asn1 = algos.EncryptionAlgorithm({ + 'algorithm': algorithm_id, + 'parameters': cms.OctetString(iv) + }) + else: + raise AS2Exception('Unsupported Encryption Algorithm') # Encrypt the key and build the ASN.1 message encrypted_key = asymmetric.rsa_pkcs1v15_encrypt(encryption_cert, key) @@ -200,7 +209,7 @@ def decrypt_message(encrypted_data, decryption_key): def sign_message(data_to_sign, digest_alg, sign_key, - use_signed_attributes=True): + sign_alg="rsassa_pkcs1v15", use_signed_attributes=True): """Function signs the data and returns the generated ASN.1 :param data_to_sign: A byte string of the data to be signed. @@ -209,7 +218,9 @@ def sign_message(data_to_sign, digest_alg, sign_key, The digest algorithm to be used for generating the signature. :param sign_key: The key to be used for generating the signature. - + + :param sign_alg: The algorithm to be used for signing the message. + :param use_signed_attributes: Optional attribute to indicate weather the CMS signature attributes should be included in the signature or not. @@ -282,10 +293,17 @@ class SmimeCapabilities(core.Sequence): ]) }), ]) - signature = asymmetric.rsa_pkcs1v15_sign(sign_key[0], signed_attributes.dump(), digest_alg) else: signed_attributes = None + + # Generate the signature + data_to_sign = signed_attributes.dump() if signed_attributes else data_to_sign + if sign_alg == "rsassa_pkcs1v15": signature = asymmetric.rsa_pkcs1v15_sign(sign_key[0], data_to_sign, digest_alg) + elif sign_alg == "rsassa_pss": + signature = asymmetric.rsa_pss_sign(sign_key[0], data_to_sign, digest_alg) + else: + raise AS2Exception('Unsupported Signature Algorithm') return cms.ContentInfo({ 'content_type': cms.ContentType('signed_data'), @@ -321,7 +339,7 @@ class SmimeCapabilities(core.Sequence): 'signed_attrs': signed_attributes, 'signature_algorithm': algos.SignedDigestAlgorithm({ 'algorithm': - algos.SignedDigestAlgorithmId('rsassa_pkcs1v15') + algos.SignedDigestAlgorithmId(sign_alg) }), 'signature': core.OctetString(signature) }) @@ -387,6 +405,8 @@ def verify_message(data_to_verify, signature, verify_cert): else: raise AS2Exception('Unsupported Signature Algorithm') except Exception as e: + import traceback + traceback.print_exc() raise IntegrityError( 'Failed to verify message signature: {}'.format(e)) else: diff --git a/pyas2lib/tests/test_advanced.py b/pyas2lib/tests/test_advanced.py index f8e6d4a..185e962 100644 --- a/pyas2lib/tests/test_advanced.py +++ b/pyas2lib/tests/test_advanced.py @@ -3,6 +3,7 @@ import os from email import message +import pytest from pyas2lib import as2 from pyas2lib.exceptions import ImproperlyConfigured from pyas2lib.tests import Pyas2TestCase, TEST_DIR @@ -387,30 +388,80 @@ def test_mdn_checks(self): assert mdn.headers == {} assert mdn.headers_str == b'' - def test_all_encryption_algos(self): - """Test all the available encryption algorithms.""" - algos = ['rc2_128_cbc', 'rc4_128_cbc', 'aes_128_cbc', 'aes_192_cbc', 'aes_256_cbc'] - - for algo in algos: - # Build an As2 message to be transmitted to partner - self.partner.encrypt = True - self.partner.enc_alg = algo - out_message = as2.Message(self.org, self.partner) - out_message.build(self.test_data) - raw_out_message = out_message.headers_str + b'\r\n' + out_message.content - - # Parse the generated AS2 message as the partner - in_message = as2.Message() - status, _, _ = in_message.parse( - raw_out_message, - find_org_cb=self.find_org, - find_partner_cb=self.find_partner - ) + def test_mdn_not_found(self): + """Test that the MDN parser raises MDN not found when a non MDN message is passed.""" + self.partner.encrypt = True + self.partner.validate_certs = False + self.partner.mdn_mode = as2.SYNCHRONOUS_MDN + self.out_message = as2.Message(self.org, self.partner) + self.out_message.build(self.test_data) - # Compare the mic contents of the input and output messages - self.assertEqual(status, 'processed') - self.assertTrue(in_message.encrypted) - self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) + # Parse the AS2 message as an MDN + mdn = as2.Mdn() + raw_out_message = self.out_message.headers_str + b'\r\n' + self.out_message.content + status, detailed_status = mdn.parse( + raw_out_message, + find_message_cb=self.find_message + ) + self.assertEqual(status, "failed/Failure") + self.assertEqual(detailed_status, "mdn-not-found") + + def test_unsigned_mdn_sent_error(self): + """Test the case where a signed mdn was expected but unsigned mdn was returned.""" + self.partner.mdn_mode = as2.SYNCHRONOUS_MDN + self.out_message = as2.Message(self.org, self.partner) + self.out_message.build(self.test_data) + + # Parse the generated AS2 message as the partner + raw_out_message = \ + self.out_message.headers_str + b'\r\n' + self.out_message.content + in_message = as2.Message() + _, _, mdn = in_message.parse( + raw_out_message, + find_org_cb=self.find_org, + find_partner_cb=self.find_partner, + find_message_cb=lambda x, y: False + ) + + # Set the mdn sig alg and parse it + self.partner.mdn_digest_alg = "sha256" + out_mdn = as2.Mdn() + status, detailed_status = out_mdn.parse( + mdn.headers_str + b'\r\n' + mdn.content, + find_message_cb=self.find_message + ) + + self.assertEqual(status, 'failed/Failure') + self.assertEqual(detailed_status, 'Expected signed MDN but unsigned MDN returned') + + def test_non_matching_mic(self): + """Test the case where a the mic in the mdn does not match the mic in the message.""" + self.partner.mdn_mode = as2.SYNCHRONOUS_MDN + self.partner.sign = True + self.out_message = as2.Message(self.org, self.partner) + self.out_message.build(self.test_data) + + # Parse the generated AS2 message as the partner + raw_out_message = \ + self.out_message.headers_str + b'\r\n' + self.out_message.content + in_message = as2.Message() + _, _, mdn = in_message.parse( + raw_out_message, + find_org_cb=self.find_org, + find_partner_cb=self.find_partner, + find_message_cb=lambda x, y: False + ) + + # Set the mdn sig alg and parse it + self.out_message.mic = b"dummy value" + out_mdn = as2.Mdn() + status, detailed_status = out_mdn.parse( + mdn.headers_str + b'\r\n' + mdn.content, + find_message_cb=self.find_message + ) + + self.assertEqual(status, 'processed/warning') + self.assertEqual(detailed_status, 'Message Integrity check failed.') def find_org(self, headers): return self.org @@ -442,7 +493,8 @@ def setUp(self): self.partner.load_verify_cert() self.partner.load_encrypt_cert() - def xtest_process_message(self): + @pytest.mark.skip(reason="no way of currently testing this") + def test_process_message(self): """ Test processing message received from Sterling Integrator""" with open(os.path.join(TEST_DIR, 'sb2bi_signed_cmp.msg'), 'rb') as msg: as2message = as2.Message() diff --git a/pyas2lib/tests/test_cms.py b/pyas2lib/tests/test_cms.py index 319c8d7..23d0069 100644 --- a/pyas2lib/tests/test_cms.py +++ b/pyas2lib/tests/test_cms.py @@ -1,12 +1,18 @@ """Module to test cms related features of pyas2lib.""" +import os + import pytest +from oscrypto import asymmetric +from pyas2lib.as2 import Organization from pyas2lib import cms from pyas2lib.exceptions import ( + AS2Exception, DecompressionError, DecryptionError, IntegrityError ) +from pyas2lib.tests import TEST_DIR INVALID_DATA = cms.cms.ContentInfo({ @@ -25,11 +31,52 @@ def test_compress(): def test_signing(): """Test the signing and verification functions.""" + # Load the signature key + with open(os.path.join(TEST_DIR, "cert_test.p12"), 'rb') as fp: + sign_key = Organization.load_key(fp.read(), "test") + with open(os.path.join(TEST_DIR, "cert_test_public.pem"), 'rb') as fp: + verify_cert = asymmetric.load_certificate(fp.read()) + + # Test failure of signature verification with pytest.raises(IntegrityError): cms.verify_message(b'data', INVALID_DATA, None) + # Test signature without signed attributes + cms.sign_message(b"data", digest_alg="sha256", sign_key=sign_key, use_signed_attributes=False) + + # Test pss signature and verification + signature = cms.sign_message( + b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pss") + cms.verify_message(b"data", signature, verify_cert) + + # Test unsupported signature alg + with pytest.raises(AS2Exception): + cms.sign_message( + b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pssa") + def test_encryption(): """Test the encryption and decryption functions.""" + with open(os.path.join(TEST_DIR, "cert_test.p12"), 'rb') as fp: + decrypt_key = Organization.load_key(fp.read(), "test") + with open(os.path.join(TEST_DIR, "cert_test_public.pem"), 'rb') as fp: + encrypt_cert = asymmetric.load_certificate(fp.read()) + with pytest.raises(DecryptionError): cms.decrypt_message(INVALID_DATA, None) + + # Test all the encryption algorithms + enc_algorithms = ['rc2_128_cbc', 'rc4_128_cbc', 'aes_128_cbc', 'aes_192_cbc', 'aes_256_cbc'] + for enc_algorithm in enc_algorithms: + encrypted_data = cms.encrypt_message(b"data", enc_algorithm, encrypt_cert) + _, decrypted_data = cms.decrypt_message(encrypted_data, decrypt_key) + assert decrypted_data == b"data" + + # Test no encryption algorithm + with pytest.raises(AS2Exception): + cms.encrypt_message(b"data", "rc5_128_cbc", encrypt_cert) + + # Test no encryption algorithm on decrypt + encrypted_data = cms.encrypt_message(b"data", "des_64_cbc", encrypt_cert) + with pytest.raises(AS2Exception): + cms.decrypt_message(encrypted_data, decrypt_key) From adbcdf8c38d7c51d5a0c3a211fbe83cb871fa1a3 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sat, 4 Apr 2020 19:35:16 +0530 Subject: [PATCH 44/66] use black to format all the files --- pyas2lib/__init__.py | 20 +- pyas2lib/as2.py | 485 +++++++++++-------- pyas2lib/cms.py | 562 +++++++++++++---------- pyas2lib/constants.py | 46 +- pyas2lib/exceptions.py | 45 +- pyas2lib/tests/__init__.py | 28 +- pyas2lib/tests/livetest_with_mecas2.py | 82 ++-- pyas2lib/tests/livetest_with_oldpyas2.py | 82 ++-- pyas2lib/tests/test_advanced.py | 281 ++++++------ pyas2lib/tests/test_basic.py | 54 ++- pyas2lib/tests/test_cms.py | 40 +- pyas2lib/tests/test_mdn.py | 55 +-- pyas2lib/tests/test_utils.py | 71 +-- pyas2lib/tests/test_with_mecas2.py | 82 ++-- pyas2lib/utils.py | 84 ++-- setup.cfg | 3 +- setup.py | 34 +- 17 files changed, 1127 insertions(+), 927 deletions(-) diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 726c472..7253cbe 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -2,23 +2,23 @@ DIGEST_ALGORITHMS, ENCRYPTION_ALGORITHMS, MDN_CONFIRM_TEXT, - MDN_FAILED_TEXT + MDN_FAILED_TEXT, ) from pyas2lib.as2 import Mdn from pyas2lib.as2 import Message from pyas2lib.as2 import Organization from pyas2lib.as2 import Partner -__version__ = '1.2.2' +__version__ = "1.2.2" __all__ = [ - 'DIGEST_ALGORITHMS', - 'ENCRYPTION_ALGORITHMS', - 'MDN_CONFIRM_TEXT', - 'MDN_FAILED_TEXT', - 'Partner', - 'Organization', - 'Message', - 'Mdn' + "DIGEST_ALGORITHMS", + "ENCRYPTION_ALGORITHMS", + "MDN_CONFIRM_TEXT", + "MDN_FAILED_TEXT", + "Partner", + "Organization", + "Message", + "Mdn", ] diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 365eb19..49d889a 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -16,7 +16,7 @@ decrypt_message, encrypt_message, sign_message, - verify_message + verify_message, ) from pyas2lib.constants import * from pyas2lib.exceptions import * @@ -29,10 +29,10 @@ quote_as2name, split_pem, unquote_as2name, - verify_certificate_chain + verify_certificate_chain, ) -logger = logging.getLogger('pyas2lib') +logger = logging.getLogger("pyas2lib") @dataclass @@ -83,8 +83,8 @@ def load_key(key_str: bytes, key_pass: str): key, cert, _ = asymmetric.load_pkcs12(key_str, key_pass) except ValueError as e: # If it fails due to invalid password raise error here - if e.args[0] == 'Password provided is invalid': - raise AS2Exception('Password not valid for Private Key.') + if e.args[0] == "Password provided is invalid": + raise AS2Exception("Password not valid for Private Key.") # if not try to parse as a pem file key, cert = None, None @@ -96,11 +96,11 @@ def load_key(key_str: bytes, key_pass: str): key = asymmetric.load_private_key(kc, key_pass) except OSError: raise AS2Exception( - 'Invalid Private Key or password is not correct.') + "Invalid Private Key or password is not correct." + ) if not key or not cert: - raise AS2Exception( - 'Invalid Private key file or Public key not included.') + raise AS2Exception("Invalid Private key file or Public key not included.") return key, cert @@ -163,9 +163,9 @@ class Partner(object): validate_certs: bool = True compress: bool = False encrypt: bool = False - enc_alg: str = 'tripledes_192_cbc' + enc_alg: str = "tripledes_192_cbc" sign: bool = False - digest_alg: str = 'sha256' + digest_alg: str = "sha256" mdn_mode: str = None mdn_digest_alg: str = None mdn_confirm_text: str = MDN_CONFIRM_TEXT @@ -177,23 +177,26 @@ def __post_init__(self): # Validations if self.digest_alg and self.digest_alg not in DIGEST_ALGORITHMS: raise ImproperlyConfigured( - f'Unsupported Digest Algorithm {self.digest_alg}, must be ' - f'one of {DIGEST_ALGORITHMS}') + f"Unsupported Digest Algorithm {self.digest_alg}, must be " + f"one of {DIGEST_ALGORITHMS}" + ) if self.enc_alg and self.enc_alg not in ENCRYPTION_ALGORITHMS: raise ImproperlyConfigured( - f'Unsupported Encryption Algorithm {self.enc_alg}, must be ' - f'one of {ENCRYPTION_ALGORITHMS}') + f"Unsupported Encryption Algorithm {self.enc_alg}, must be " + f"one of {ENCRYPTION_ALGORITHMS}" + ) if self.mdn_mode and self.mdn_mode not in MDN_MODES: raise ImproperlyConfigured( - f'Unsupported MDN Mode {self.mdn_mode}, must be ' - f'one of {MDN_MODES}') + f"Unsupported MDN Mode {self.mdn_mode}, must be " f"one of {MDN_MODES}" + ) if self.mdn_digest_alg and self.mdn_digest_alg not in DIGEST_ALGORITHMS: raise ImproperlyConfigured( - f'Unsupported MDN Digest Algorithm {self.mdn_digest_alg}, ' - f'must be one of {DIGEST_ALGORITHMS}') + f"Unsupported MDN Digest Algorithm {self.mdn_digest_alg}, " + f"must be one of {DIGEST_ALGORITHMS}" + ) def load_verify_cert(self): if self.validate_certs: @@ -208,7 +211,8 @@ def load_verify_cert(self): # Verify the certificate against the trusted roots verify_certificate_chain( - cert, trust_roots, ignore_self_signed=self.ignore_self_signed) + cert, trust_roots, ignore_self_signed=self.ignore_self_signed + ) return asymmetric.load_certificate(self.verify_cert) @@ -225,7 +229,8 @@ def load_encrypt_cert(self): # Verify the certificate against the trusted roots verify_certificate_chain( - cert, trust_roots, ignore_self_signed=self.ignore_self_signed) + cert, trust_roots, ignore_self_signed=self.ignore_self_signed + ) return asymmetric.load_certificate(self.encrypt_cert) @@ -260,11 +265,11 @@ def __init__(self, sender=None, receiver=None): def content(self): """Function returns the body of the as2 payload as a bytes object""" if self.payload is None: - return '' + return "" if self.payload.is_multipart(): message_bytes = mime_to_bytes(self.payload) - boundary = b'--' + self.payload.get_boundary().encode('utf-8') + boundary = b"--" + self.payload.get_boundary().encode("utf-8") temp = message_bytes.split(boundary) temp.pop(0) return boundary + boundary.join(temp) @@ -281,15 +286,21 @@ def headers(self): @property def headers_str(self): - message_header = '' + message_header = "" if self.payload: for k, v in self.headers.items(): - message_header += f'{k}: {v}\r\n' - return message_header.encode('utf-8') - - def build(self, data, filename=None, subject='AS2 Message', - content_type='application/edi-consent', additional_headers=None, - disposition_notification_to='no-reply@pyas2.com'): + message_header += f"{k}: {v}\r\n" + return message_header.encode("utf-8") + + def build( + self, + data, + filename=None, + subject="AS2 Message", + content_type="application/edi-consent", + additional_headers=None, + disposition_notification_to="no-reply@pyas2.com", + ): """Function builds the AS2 message. Compresses, signs and encrypts the payload if applicable. @@ -317,31 +328,33 @@ def build(self, data, filename=None, subject='AS2 Message', """ # Validations - assert type(data) is bytes, 'Parameter data must be of bytes type.' + assert type(data) is bytes, "Parameter data must be of bytes type." additional_headers = additional_headers if additional_headers else {} assert type(additional_headers) is dict if self.receiver.sign and not self.sender.sign_key: raise ImproperlyConfigured( - 'Signing of messages is enabled but sign key is not set for the sender.') + "Signing of messages is enabled but sign key is not set for the sender." + ) if self.receiver.encrypt and not self.receiver.encrypt_cert: raise ImproperlyConfigured( - 'Encryption of messages is enabled but encrypt key is not set for the receiver.') + "Encryption of messages is enabled but encrypt key is not set for the receiver." + ) # Generate message id using UUID 1 as it uses both hostname and time - self.message_id = email_utils.make_msgid().lstrip('<').rstrip('>') + self.message_id = email_utils.make_msgid().lstrip("<").rstrip(">") # Set up the message headers as2_headers = { - 'AS2-Version': AS2_VERSION, - 'ediint-features': EDIINT_FEATURES, - 'Message-ID': f'<{self.message_id}>', - 'AS2-From': quote_as2name(self.sender.as2_name), - 'AS2-To': quote_as2name(self.receiver.as2_name), - 'Subject': subject, - 'Date': email_utils.formatdate(localtime=True) + "AS2-Version": AS2_VERSION, + "ediint-features": EDIINT_FEATURES, + "Message-ID": f"<{self.message_id}>", + "AS2-From": quote_as2name(self.sender.as2_name), + "AS2-To": quote_as2name(self.receiver.as2_name), + "Subject": subject, + "Date": email_utils.formatdate(localtime=True), } as2_headers.update(additional_headers) @@ -353,27 +366,36 @@ def build(self, data, filename=None, subject='AS2 Message', encoders.encode_7or8bit(self.payload) if filename: - self.payload.add_header('Content-Disposition', 'attachment', filename=filename) - del self.payload['MIME-Version'] + self.payload.add_header( + "Content-Disposition", "attachment", filename=filename + ) + del self.payload["MIME-Version"] if self.receiver.compress: self.compressed = True compressed_message = email_message.Message() - compressed_message.set_type('application/pkcs7-mime') - compressed_message.set_param('name', 'smime.p7z') - compressed_message.set_param('smime-type', 'compressed-data') - compressed_message.add_header('Content-Disposition', 'attachment', filename='smime.p7z') - compressed_message.add_header('Content-Transfer-Encoding', 'binary') - compressed_message.set_payload(compress_message(mime_to_bytes(self.payload))) + compressed_message.set_type("application/pkcs7-mime") + compressed_message.set_param("name", "smime.p7z") + compressed_message.set_param("smime-type", "compressed-data") + compressed_message.add_header( + "Content-Disposition", "attachment", filename="smime.p7z" + ) + compressed_message.add_header("Content-Transfer-Encoding", "binary") + compressed_message.set_payload( + compress_message(mime_to_bytes(self.payload)) + ) self.payload = compressed_message logger.debug( - f'Compressed message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}') + f"Compressed message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}" + ) if self.receiver.sign: self.signed, self.digest_alg = True, self.receiver.digest_alg - signed_message = MIMEMultipart('signed', protocol="application/pkcs7-signature") - del signed_message['MIME-Version'] + signed_message = MIMEMultipart( + "signed", protocol="application/pkcs7-signature" + ) + del signed_message["MIME-Version"] signed_message.attach(self.payload) # Calculate the MIC Hash of the message to be verified @@ -384,50 +406,61 @@ def build(self, data, filename=None, subject='AS2 Message', # Create the signature mime message signature = email_message.Message() - signature.set_type('application/pkcs7-signature') - signature.set_param('name', 'smime.p7s') - signature.set_param('smime-type', 'signed-data') - signature.add_header('Content-Disposition', 'attachment', filename='smime.p7s') - del signature['MIME-Version'] - signature_data = sign_message(mic_content, self.digest_alg, self.sender.sign_key) + signature.set_type("application/pkcs7-signature") + signature.set_param("name", "smime.p7s") + signature.set_param("smime-type", "signed-data") + signature.add_header( + "Content-Disposition", "attachment", filename="smime.p7s" + ) + del signature["MIME-Version"] + signature_data = sign_message( + mic_content, self.digest_alg, self.sender.sign_key + ) signature.set_payload(signature_data) encoders.encode_base64(signature) - signed_message.set_param('micalg', self.digest_alg) + signed_message.set_param("micalg", self.digest_alg) signed_message.attach(signature) self.payload = signed_message logger.debug( - f'Signed message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}') + f"Signed message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}" + ) if self.receiver.encrypt: self.encrypted, self.enc_alg = True, self.receiver.enc_alg encrypted_message = email_message.Message() - encrypted_message.set_type('application/pkcs7-mime') - encrypted_message.set_param('name', 'smime.p7m') - encrypted_message.set_param('smime-type', 'enveloped-data') - encrypted_message.add_header('Content-Disposition', 'attachment', filename='smime.p7m') - encrypted_message.add_header('Content-Transfer-Encoding', 'binary') + encrypted_message.set_type("application/pkcs7-mime") + encrypted_message.set_param("name", "smime.p7m") + encrypted_message.set_param("smime-type", "enveloped-data") + encrypted_message.add_header( + "Content-Disposition", "attachment", filename="smime.p7m" + ) + encrypted_message.add_header("Content-Transfer-Encoding", "binary") encrypt_cert = self.receiver.load_encrypt_cert() encrypted_data = encrypt_message( - mime_to_bytes(self.payload), self.enc_alg, encrypt_cert) + mime_to_bytes(self.payload), self.enc_alg, encrypt_cert + ) encrypted_message.set_payload(encrypted_data) self.payload = encrypted_message logger.debug( - f'Encrypted message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}') + f"Encrypted message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}" + ) if self.receiver.mdn_mode: - as2_headers['disposition-notification-to'] = disposition_notification_to + as2_headers["disposition-notification-to"] = disposition_notification_to if self.receiver.mdn_digest_alg: - as2_headers['disposition-notification-options'] = \ - f'signed-receipt-protocol=required, pkcs7-signature; ' \ - f'signed-receipt-micalg=optional, {self.receiver.mdn_digest_alg}' - if self.receiver.mdn_mode == 'ASYNC': + as2_headers["disposition-notification-options"] = ( + f"signed-receipt-protocol=required, pkcs7-signature; " + f"signed-receipt-micalg=optional, {self.receiver.mdn_digest_alg}" + ) + if self.receiver.mdn_mode == "ASYNC": if not self.sender.mdn_url: raise ImproperlyConfigured( - 'MDN URL must be set in the organization when MDN mode is set to ASYNC') - as2_headers['receipt-delivery-option'] = self.sender.mdn_url + "MDN URL must be set in the organization when MDN mode is set to ASYNC" + ) + as2_headers["receipt-delivery-option"] = self.sender.mdn_url # Update the headers of the final payload and set its boundary for k, v in as2_headers.items(): @@ -440,18 +473,21 @@ def build(self, data, filename=None, subject='AS2 Message', self.payload.set_boundary(make_mime_boundary()) def _decompress_data(self, payload): - if payload.get_content_type() == 'application/pkcs7-mime' \ - and payload.get_param('smime-type') == 'compressed-data': - logger.debug(f'Decompressing message {self.message_id} payload :\n' - f'{mime_to_bytes(self.payload)}') + if ( + payload.get_content_type() == "application/pkcs7-mime" + and payload.get_param("smime-type") == "compressed-data" + ): + logger.debug( + f"Decompressing message {self.message_id} payload :\n" + f"{mime_to_bytes(self.payload)}" + ) compressed_data = payload.get_payload(decode=True) decompressed_data = decompress_message(compressed_data) return True, parse_mime(decompressed_data) return False, payload - def parse(self, raw_content, find_org_cb, find_partner_cb, - find_message_cb=None): + def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None): """Function parses the RAW AS2 message; decrypts, verifies and decompresses it and extracts the payload. @@ -480,71 +516,88 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, """ # Parse the raw MIME message and extract its content and headers - status, detailed_status, exception, mdn = \ - 'processed', None, (None, None), None + status, detailed_status, exception, mdn = "processed", None, (None, None), None self.payload = parse_mime(raw_content) as2_headers = {} for k, v in self.payload.items(): k = k.lower() - if k == 'message-id': - self.message_id = v.lstrip('<').rstrip('>') + if k == "message-id": + self.message_id = v.lstrip("<").rstrip(">") as2_headers[k] = v try: # Get the organization and partner for this transmission - org_id = unquote_as2name(as2_headers['as2-to']) + org_id = unquote_as2name(as2_headers["as2-to"]) self.receiver = find_org_cb(org_id) if not self.receiver: - raise PartnerNotFound(f'Unknown AS2 organization with id {org_id}') + raise PartnerNotFound(f"Unknown AS2 organization with id {org_id}") - partner_id = unquote_as2name(as2_headers['as2-from']) + partner_id = unquote_as2name(as2_headers["as2-from"]) self.sender = find_partner_cb(partner_id) if not self.sender: - raise PartnerNotFound(f'Unknown AS2 partner with id {partner_id}') + raise PartnerNotFound(f"Unknown AS2 partner with id {partner_id}") if find_message_cb and find_message_cb(self.message_id, partner_id): raise DuplicateDocument( - 'Duplicate message received, message with this ID already processed.') + "Duplicate message received, message with this ID already processed." + ) - if self.sender.encrypt and \ - self.payload.get_content_type() != 'application/pkcs7-mime': + if ( + self.sender.encrypt + and self.payload.get_content_type() != "application/pkcs7-mime" + ): raise InsufficientSecurityError( - f'Incoming messages from partner {partner_id} are must be encrypted ' - f'but encrypted message not found.') - - if self.payload.get_content_type() == 'application/pkcs7-mime' \ - and self.payload.get_param('smime-type') == 'enveloped-data': - logger.debug(f'Decrypting message {self.message_id} payload :\n' - f'{mime_to_bytes(self.payload)}') + f"Incoming messages from partner {partner_id} are must be encrypted " + f"but encrypted message not found." + ) + + if ( + self.payload.get_content_type() == "application/pkcs7-mime" + and self.payload.get_param("smime-type") == "enveloped-data" + ): + logger.debug( + f"Decrypting message {self.message_id} payload :\n" + f"{mime_to_bytes(self.payload)}" + ) self.encrypted = True encrypted_data = self.payload.get_payload(decode=True) self.enc_alg, decrypted_content = decrypt_message( - encrypted_data, self.receiver.decrypt_key) + encrypted_data, self.receiver.decrypt_key + ) self.payload = parse_mime(decrypted_content) - if self.payload.get_content_type() == 'text/plain': + if self.payload.get_content_type() == "text/plain": self.payload = email_message.Message() self.payload.set_payload(decrypted_content) - self.payload.set_type('application/edi-consent') + self.payload.set_type("application/edi-consent") # Check for compressed data here self.compressed, self.payload = self._decompress_data(self.payload) - if self.sender.sign and self.payload.get_content_type() != 'multipart/signed': + if ( + self.sender.sign + and self.payload.get_content_type() != "multipart/signed" + ): raise InsufficientSecurityError( - f'Incoming messages from partner {partner_id} are must be signed ' - f'but signed message not found.') - - if self.payload.get_content_type() == 'multipart/signed': - logger.debug(f'Verifying signed message {self.message_id} payload: \n' - f'{mime_to_bytes(self.payload)}') + f"Incoming messages from partner {partner_id} are must be signed " + f"but signed message not found." + ) + + if self.payload.get_content_type() == "multipart/signed": + logger.debug( + f"Verifying signed message {self.message_id} payload: \n" + f"{mime_to_bytes(self.payload)}" + ) self.signed = True # Split the message into signature and signed message signature = None - signature_types = ['application/pkcs7-signature', 'application/x-pkcs7-signature'] + signature_types = [ + "application/pkcs7-signature", + "application/x-pkcs7-signature", + ] for part in self.payload.walk(): if part.get_content_type() in signature_types: signature = part.get_payload(decode=True) @@ -567,31 +620,35 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, self.compressed, self.payload = self._decompress_data(self.payload) except Exception as e: - status = getattr(e, 'disposition_type', 'processed/Error') - detailed_status = getattr(e, 'disposition_modifier', 'unexpected-processing-error') + status = getattr(e, "disposition_type", "processed/Error") + detailed_status = getattr( + e, "disposition_modifier", "unexpected-processing-error" + ) exception = (e, traceback.format_exc()) - logger.error(f'Failed to parse AS2 message\n: {traceback.format_exc()}') + logger.error(f"Failed to parse AS2 message\n: {traceback.format_exc()}") finally: # Update the payload headers with the original headers for k, v in as2_headers.items(): - if self.payload.get(k) and k.lower() != 'content-disposition': + if self.payload.get(k) and k.lower() != "content-disposition": del self.payload[k] self.payload.add_header(k, v) - if as2_headers.get('disposition-notification-to'): + if as2_headers.get("disposition-notification-to"): mdn_mode = SYNCHRONOUS_MDN - mdn_url = as2_headers.get('receipt-delivery-option') + mdn_url = as2_headers.get("receipt-delivery-option") if mdn_url: mdn_mode = ASYNCHRONOUS_MDN - digest_alg = as2_headers.get('disposition-notification-options') + digest_alg = as2_headers.get("disposition-notification-options") if digest_alg: - digest_alg = digest_alg.split(';')[-1].split(',')[-1].strip() + digest_alg = digest_alg.split(";")[-1].split(",")[-1].strip() - logger.debug(f'Building the MDN for message {self.message_id} with status {status} ' - f'and detailed-status {detailed_status}.') + logger.debug( + f"Building the MDN for message {self.message_id} with status {status} " + f"and detailed-status {detailed_status}." + ) mdn = Mdn(mdn_mode=mdn_mode, mdn_url=mdn_url, digest_alg=digest_alg) mdn.build(message=self, status=status, detailed_status=detailed_status) @@ -617,12 +674,12 @@ def content(self): if self.payload is not None: message_bytes = mime_to_bytes(self.payload) - boundary = b'--' + self.payload.get_boundary().encode('utf-8') + boundary = b"--" + self.payload.get_boundary().encode("utf-8") temp = message_bytes.split(boundary) temp.pop(0) return boundary + boundary.join(temp) else: - return '' + return "" @property def headers(self): @@ -633,14 +690,20 @@ def headers(self): @property def headers_str(self): - message_header = '' + message_header = "" if self.payload: for k, v in self.headers.items(): - message_header += f'{k}: {v}\r\n' - return message_header.encode('utf-8') - - def build(self, message, status, detailed_status=None, confirmation_text=MDN_CONFIRM_TEXT, - failed_text=MDN_FAILED_TEXT): + message_header += f"{k}: {v}\r\n" + return message_header.encode("utf-8") + + def build( + self, + message, + status, + detailed_status=None, + confirmation_text=MDN_CONFIRM_TEXT, + failed_text=MDN_FAILED_TEXT, + ): """Function builds and signs an AS2 MDN message. :param message: The received AS2 message for which this is an MDN. @@ -656,18 +719,18 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON """ # Generate message id using UUID 1 as it uses both hostname and time - self.message_id = email_utils.make_msgid().lstrip('<').rstrip('>') + self.message_id = email_utils.make_msgid().lstrip("<").rstrip(">") self.orig_message_id = message.message_id # Set up the message headers mdn_headers = { - 'AS2-Version': AS2_VERSION, - 'ediint-features': EDIINT_FEATURES, - 'Message-ID': f'<{self.message_id}>', - 'AS2-From': quote_as2name(message.headers.get('as2-to')), - 'AS2-To': quote_as2name(message.headers.get('as2-from')), - 'Date': email_utils.formatdate(localtime=True), - 'user-agent': 'pyAS2 Open Source AS2 Software' + "AS2-Version": AS2_VERSION, + "ediint-features": EDIINT_FEATURES, + "Message-ID": f"<{self.message_id}>", + "AS2-From": quote_as2name(message.headers.get("as2-to")), + "AS2-To": quote_as2name(message.headers.get("as2-from")), + "Date": email_utils.formatdate(localtime=True), + "user-agent": "pyAS2 Open Source AS2 Software", } # Set the confirmation text message here @@ -679,56 +742,67 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON if message.sender and message.sender.mdn_confirm_text: confirmation_text = message.sender.mdn_confirm_text - if status != 'processed': + if status != "processed": confirmation_text = failed_text - self.payload = MIMEMultipart('report', report_type='disposition-notification') + self.payload = MIMEMultipart("report", report_type="disposition-notification") # Create and attach the MDN Text Message mdn_text = email_message.Message() - mdn_text.set_payload(f'{confirmation_text}\r\n') - mdn_text.set_type('text/plain') - del mdn_text['MIME-Version'] + mdn_text.set_payload(f"{confirmation_text}\r\n") + mdn_text.set_type("text/plain") + del mdn_text["MIME-Version"] encoders.encode_7or8bit(mdn_text) self.payload.attach(mdn_text) # Create and attache the MDN Report Message mdn_base = email_message.Message() - mdn_base.set_type('message/disposition-notification') - mdn_report = 'Reporting-UA: pyAS2 Open Source AS2 Software\r\n' + mdn_base.set_type("message/disposition-notification") + mdn_report = "Reporting-UA: pyAS2 Open Source AS2 Software\r\n" mdn_report += f'Original-Recipient: rfc822; {message.headers.get("as2-to")}\r\n' mdn_report += f'Final-Recipient: rfc822; {message.headers.get("as2-to")}\r\n' - mdn_report += f'Original-Message-ID: <{message.message_id}>\r\n' - mdn_report += f'Disposition: automatic-action/MDN-sent-automatically; {status}' + mdn_report += f"Original-Message-ID: <{message.message_id}>\r\n" + mdn_report += f"Disposition: automatic-action/MDN-sent-automatically; {status}" if detailed_status: - mdn_report += f': {detailed_status}' - mdn_report += '\r\n' + mdn_report += f": {detailed_status}" + mdn_report += "\r\n" if message.mic: - mdn_report += f'Received-content-MIC: {message.mic.decode()}, {message.digest_alg}\r\n' + mdn_report += f"Received-content-MIC: {message.mic.decode()}, {message.digest_alg}\r\n" mdn_base.set_payload(mdn_report) - del mdn_base['MIME-Version'] + del mdn_base["MIME-Version"] encoders.encode_7or8bit(mdn_base) self.payload.attach(mdn_base) logger.debug( - f'MDN report for message {message.message_id} created:\n{mime_to_bytes(mdn_base)}') + f"MDN report for message {message.message_id} created:\n{mime_to_bytes(mdn_base)}" + ) # Sign the MDN if it is requested by the sender - if message.headers.get('disposition-notification-options') and \ - message.receiver and message.receiver.sign_key: - self.digest_alg = message.headers['disposition-notification-options'].\ - split(';')[-1].split(',')[-1].strip().replace('-', '') - signed_mdn = MIMEMultipart('signed', protocol="application/pkcs7-signature") - del signed_mdn['MIME-Version'] + if ( + message.headers.get("disposition-notification-options") + and message.receiver + and message.receiver.sign_key + ): + self.digest_alg = ( + message.headers["disposition-notification-options"] + .split(";")[-1] + .split(",")[-1] + .strip() + .replace("-", "") + ) + signed_mdn = MIMEMultipart("signed", protocol="application/pkcs7-signature") + del signed_mdn["MIME-Version"] signed_mdn.attach(self.payload) # Create the signature mime message signature = email_message.Message() - signature.set_type('application/pkcs7-signature') - signature.set_param('name', 'smime.p7s') - signature.set_param('smime-type', 'signed-data') - signature.add_header('Content-Disposition', 'attachment', filename='smime.p7s') - del signature['MIME-Version'] + signature.set_type("application/pkcs7-signature") + signature.set_param("name", "smime.p7s") + signature.set_param("smime-type", "signed-data") + signature.add_header( + "Content-Disposition", "attachment", filename="smime.p7s" + ) + del signature["MIME-Version"] signed_data = sign_message( canonicalize(self.payload), self.digest_alg, message.receiver.sign_key @@ -736,11 +810,11 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON signature.set_payload(signed_data) encoders.encode_base64(signature) - signed_mdn.set_param('micalg', self.digest_alg) + signed_mdn.set_param("micalg", self.digest_alg) signed_mdn.attach(signature) self.payload = signed_mdn - logger.debug(f'Signing the MDN for message {message.message_id}') + logger.debug(f"Signing the MDN for message {message.message_id}") # Update the headers of the final payload and set message boundary for k, v in mdn_headers.items(): @@ -749,8 +823,10 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON else: self.payload.add_header(k, v) self.payload.set_boundary(make_mime_boundary()) - logger.debug(f'MDN generated for message {message.message_id} with ' - f'content:\n {mime_to_bytes(self.payload)}') + logger.debug( + f"MDN generated for message {message.message_id} with " + f"content:\n {mime_to_bytes(self.payload)}" + ) def parse(self, raw_content, find_message_cb): """Function parses the RAW AS2 MDN, verifies it and extracts the @@ -783,27 +859,32 @@ def parse(self, raw_content, find_message_cb): mdn_headers = {} for k, v in self.payload.items(): k = k.lower() - if k == 'message-id': - self.message_id = v.lstrip('<').rstrip('>') + if k == "message-id": + self.message_id = v.lstrip("<").rstrip(">") mdn_headers[k] = v - if orig_message.receiver.mdn_digest_alg \ - and self.payload.get_content_type() != 'multipart/signed': - status = 'failed/Failure' - detailed_status = 'Expected signed MDN but unsigned MDN returned' + if ( + orig_message.receiver.mdn_digest_alg + and self.payload.get_content_type() != "multipart/signed" + ): + status = "failed/Failure" + detailed_status = "Expected signed MDN but unsigned MDN returned" return status, detailed_status - if self.payload.get_content_type() == 'multipart/signed': - logger.debug(f'Verifying signed MDN: \n{mime_to_bytes(self.payload)}') - message_boundary = ('--' + self.payload.get_boundary()).encode('utf-8') + if self.payload.get_content_type() == "multipart/signed": + logger.debug(f"Verifying signed MDN: \n{mime_to_bytes(self.payload)}") + message_boundary = ("--" + self.payload.get_boundary()).encode("utf-8") # Extract the signature and the signed payload signature = None - signature_types = ['application/pkcs7-signature', 'application/x-pkcs7-signature'] + signature_types = [ + "application/pkcs7-signature", + "application/x-pkcs7-signature", + ] for part in self.payload.walk(): if part.get_content_type() in signature_types: signature = part.get_payload(decode=True) - elif part.get_content_type() == 'multipart/report': + elif part.get_content_type() == "multipart/report": self.payload = part # Verify the message, first using raw message and if it fails @@ -811,34 +892,43 @@ def parse(self, raw_content, find_message_cb): mic_content = extract_first_part(raw_content, message_boundary) verify_cert = orig_message.receiver.load_verify_cert() try: - self.digest_alg = verify_message(mic_content, signature, verify_cert) + self.digest_alg = verify_message( + mic_content, signature, verify_cert + ) except IntegrityError: mic_content = canonicalize(self.payload) - self.digest_alg = verify_message(mic_content, signature, verify_cert) + self.digest_alg = verify_message( + mic_content, signature, verify_cert + ) for part in self.payload.walk(): - if part.get_content_type() == 'message/disposition-notification': + if part.get_content_type() == "message/disposition-notification": logger.debug( - f'MDN report for message {orig_message.message_id}:\n{part.as_string()}') + f"MDN report for message {orig_message.message_id}:\n{part.as_string()}" + ) mdn = part.get_payload()[-1] - mdn_status = mdn['Disposition'].split(';').pop().strip().split(':') + mdn_status = mdn["Disposition"].split(";").pop().strip().split(":") status = mdn_status[0] - if status == 'processed': + if status == "processed": # Compare the original mic with the received mic - mdn_mic = mdn.get('Received-Content-MIC', '').split(',')[0] - if mdn_mic and orig_message.mic and mdn_mic != orig_message.mic.decode(): - status = 'processed/warning' - detailed_status = 'Message Integrity check failed.' + mdn_mic = mdn.get("Received-Content-MIC", "").split(",")[0] + if ( + mdn_mic + and orig_message.mic + and mdn_mic != orig_message.mic.decode() + ): + status = "processed/warning" + detailed_status = "Message Integrity check failed." else: - detailed_status = ' '.join(mdn_status[1:]).strip() + detailed_status = " ".join(mdn_status[1:]).strip() except MDNNotFound: - status = 'failed/Failure' - detailed_status = 'mdn-not-found' + status = "failed/Failure" + detailed_status = "mdn-not-found" except Exception as e: - status = 'failed/Failure' - detailed_status = f'Failed to parse received MDN. {e}' - logger.error(f'Failed to parse AS2 MDN\n: {traceback.format_exc()}') + status = "failed/Failure" + detailed_status = f"Failed to parse received MDN. {e}" + logger.error(f"Failed to parse AS2 MDN\n: {traceback.format_exc()}") finally: return status, detailed_status @@ -855,21 +945,20 @@ def detect_mdn(self): is the original AS2 message recipient. """ mdn_message = None - if self.payload.get_content_type() == 'multipart/report': + if self.payload.get_content_type() == "multipart/report": mdn_message = self.payload - elif self.payload.get_content_type() == 'multipart/signed': + elif self.payload.get_content_type() == "multipart/signed": for part in self.payload.walk(): - if part.get_content_type() == 'multipart/report': + if part.get_content_type() == "multipart/report": mdn_message = self.payload if not mdn_message: - raise MDNNotFound('No MDN found in the received message') + raise MDNNotFound("No MDN found in the received message") message_id, message_recipient = None, None for part in mdn_message.walk(): - if part.get_content_type() == 'message/disposition-notification': + if part.get_content_type() == "message/disposition-notification": mdn = part.get_payload()[0] - message_id = mdn.get('Original-Message-ID').strip('<>') - message_recipient = mdn.get('Original-Recipient').\ - split(';')[1].strip() + message_id = mdn.get("Original-Message-ID").strip("<>") + message_recipient = mdn.get("Original-Recipient").split(";")[1].strip() return message_id, message_recipient diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 9df5f5f..3a913e4 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -17,22 +17,26 @@ def compress_message(data_to_compress): :return: A CMS ASN.1 byte string of the compressed data. """ - compressed_content = cms.ParsableOctetString( - zlib.compress(data_to_compress)) - return cms.ContentInfo({ - 'content_type': cms.ContentType('compressed_data'), - 'content': cms.CompressedData({ - 'version': cms.CMSVersion('v0'), - 'compression_algorithm': - cms.CompressionAlgorithm({ - 'algorithm': cms.CompressionAlgorithmId('zlib') - }), - 'encap_content_info': cms.EncapsulatedContentInfo({ - 'content_type': cms.ContentType('data'), - 'content': compressed_content - }) - }) - }).dump() + compressed_content = cms.ParsableOctetString(zlib.compress(data_to_compress)) + return cms.ContentInfo( + { + "content_type": cms.ContentType("compressed_data"), + "content": cms.CompressedData( + { + "version": cms.CMSVersion("v0"), + "compression_algorithm": cms.CompressionAlgorithm( + {"algorithm": cms.CompressionAlgorithmId("zlib")} + ), + "encap_content_info": cms.EncapsulatedContentInfo( + { + "content_type": cms.ContentType("data"), + "content": compressed_content, + } + ), + } + ), + } + ).dump() def decompress_message(compressed_data): @@ -46,13 +50,13 @@ def decompress_message(compressed_data): """ try: cms_content = cms.ContentInfo.load(compressed_data) - if cms_content['content_type'].native == 'compressed_data': - return cms_content['content'].decompressed + if cms_content["content_type"].native == "compressed_data": + return cms_content["content"].decompressed else: - raise DecompressionError('Compressed data not found in ASN.1 ') + raise DecompressionError("Compressed data not found in ASN.1 ") except Exception as e: - raise DecompressionError('Decompression failed with cause: {}'.format(e)) + raise DecompressionError("Decompression failed with cause: {}".format(e)) def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): @@ -67,88 +71,107 @@ def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): :return: A CMS ASN.1 byte string of the encrypted data. """ - enc_alg_list = enc_alg.split('_') + enc_alg_list = enc_alg.split("_") cipher, key_length, mode = enc_alg_list[0], enc_alg_list[1], enc_alg_list[2] # Generate the symmetric encryption key and encrypt the message key = util.rand_bytes(int(key_length) // 8) - if cipher == 'tripledes': - algorithm_id = '1.2.840.113549.3.7' - iv, encrypted_content = symmetric.tripledes_cbc_pkcs5_encrypt(key, data_to_encrypt, None) - enc_alg_asn1 = algos.EncryptionAlgorithm({ - 'algorithm': algorithm_id, - 'parameters': cms.OctetString(iv) - }) - - elif cipher == 'rc2': - algorithm_id = '1.2.840.113549.3.2' - iv, encrypted_content = symmetric.rc2_cbc_pkcs5_encrypt(key, data_to_encrypt, None) - enc_alg_asn1 = algos.EncryptionAlgorithm({ - 'algorithm': algorithm_id, - 'parameters': algos.Rc2Params({'iv': cms.OctetString(iv)}) - }) - - elif cipher == 'rc4': - algorithm_id = '1.2.840.113549.3.4' + if cipher == "tripledes": + algorithm_id = "1.2.840.113549.3.7" + iv, encrypted_content = symmetric.tripledes_cbc_pkcs5_encrypt( + key, data_to_encrypt, None + ) + enc_alg_asn1 = algos.EncryptionAlgorithm( + {"algorithm": algorithm_id, "parameters": cms.OctetString(iv)} + ) + + elif cipher == "rc2": + algorithm_id = "1.2.840.113549.3.2" + iv, encrypted_content = symmetric.rc2_cbc_pkcs5_encrypt( + key, data_to_encrypt, None + ) + enc_alg_asn1 = algos.EncryptionAlgorithm( + { + "algorithm": algorithm_id, + "parameters": algos.Rc2Params({"iv": cms.OctetString(iv)}), + } + ) + + elif cipher == "rc4": + algorithm_id = "1.2.840.113549.3.4" encrypted_content = symmetric.rc4_encrypt(key, data_to_encrypt) - enc_alg_asn1 = algos.EncryptionAlgorithm({ - 'algorithm': algorithm_id, - }) - - elif cipher == 'aes': - if key_length == '128': - algorithm_id = '2.16.840.1.101.3.4.1.2' - elif key_length == '192': - algorithm_id = '2.16.840.1.101.3.4.1.22' + enc_alg_asn1 = algos.EncryptionAlgorithm({"algorithm": algorithm_id,}) + + elif cipher == "aes": + if key_length == "128": + algorithm_id = "2.16.840.1.101.3.4.1.2" + elif key_length == "192": + algorithm_id = "2.16.840.1.101.3.4.1.22" else: - algorithm_id = '2.16.840.1.101.3.4.1.42' - - iv, encrypted_content = symmetric.aes_cbc_pkcs7_encrypt(key, data_to_encrypt, None) - enc_alg_asn1 = algos.EncryptionAlgorithm({ - 'algorithm': algorithm_id, - 'parameters': cms.OctetString(iv) - }) - elif cipher == 'des': - algorithm_id = '1.3.14.3.2.7' - iv, encrypted_content = symmetric.des_cbc_pkcs5_encrypt(key, data_to_encrypt, None) - enc_alg_asn1 = algos.EncryptionAlgorithm({ - 'algorithm': algorithm_id, - 'parameters': cms.OctetString(iv) - }) + algorithm_id = "2.16.840.1.101.3.4.1.42" + + iv, encrypted_content = symmetric.aes_cbc_pkcs7_encrypt( + key, data_to_encrypt, None + ) + enc_alg_asn1 = algos.EncryptionAlgorithm( + {"algorithm": algorithm_id, "parameters": cms.OctetString(iv)} + ) + elif cipher == "des": + algorithm_id = "1.3.14.3.2.7" + iv, encrypted_content = symmetric.des_cbc_pkcs5_encrypt( + key, data_to_encrypt, None + ) + enc_alg_asn1 = algos.EncryptionAlgorithm( + {"algorithm": algorithm_id, "parameters": cms.OctetString(iv)} + ) else: - raise AS2Exception('Unsupported Encryption Algorithm') + raise AS2Exception("Unsupported Encryption Algorithm") # Encrypt the key and build the ASN.1 message encrypted_key = asymmetric.rsa_pkcs1v15_encrypt(encryption_cert, key) - return cms.ContentInfo({ - 'content_type': cms.ContentType('enveloped_data'), - 'content': cms.EnvelopedData({ - 'version': cms.CMSVersion('v0'), - 'recipient_infos': [ - cms.KeyTransRecipientInfo({ - 'version': cms.CMSVersion('v0'), - 'rid': cms.RecipientIdentifier({ - 'issuer_and_serial_number': cms.IssuerAndSerialNumber({ - 'issuer': encryption_cert.asn1[ - 'tbs_certificate']['issuer'], - 'serial_number': encryption_cert.asn1[ - 'tbs_certificate']['serial_number'] - }) - }), - 'key_encryption_algorithm': cms.KeyEncryptionAlgorithm({ - 'algorithm': cms.KeyEncryptionAlgorithmId('rsa') - }), - 'encrypted_key': cms.OctetString(encrypted_key) - }) - ], - 'encrypted_content_info': cms.EncryptedContentInfo({ - 'content_type': cms.ContentType('data'), - 'content_encryption_algorithm': enc_alg_asn1, - 'encrypted_content': encrypted_content - }) - }) - }).dump() + return cms.ContentInfo( + { + "content_type": cms.ContentType("enveloped_data"), + "content": cms.EnvelopedData( + { + "version": cms.CMSVersion("v0"), + "recipient_infos": [ + cms.KeyTransRecipientInfo( + { + "version": cms.CMSVersion("v0"), + "rid": cms.RecipientIdentifier( + { + "issuer_and_serial_number": cms.IssuerAndSerialNumber( + { + "issuer": encryption_cert.asn1[ + "tbs_certificate" + ]["issuer"], + "serial_number": encryption_cert.asn1[ + "tbs_certificate" + ]["serial_number"], + } + ) + } + ), + "key_encryption_algorithm": cms.KeyEncryptionAlgorithm( + {"algorithm": cms.KeyEncryptionAlgorithmId("rsa")} + ), + "encrypted_key": cms.OctetString(encrypted_key), + } + ) + ], + "encrypted_content_info": cms.EncryptedContentInfo( + { + "content_type": cms.ContentType("data"), + "content_encryption_algorithm": enc_alg_asn1, + "encrypted_content": encrypted_content, + } + ), + } + ), + } + ).dump() def decrypt_message(encrypted_data, decryption_key): @@ -166,50 +189,63 @@ def decrypt_message(encrypted_data, decryption_key): cms_content = cms.ContentInfo.load(encrypted_data) cipher, decrypted_content = None, None - if cms_content['content_type'].native == 'enveloped_data': - recipient_info = cms_content['content']['recipient_infos'][0].parse() - key_enc_alg = recipient_info[ - 'key_encryption_algorithm']['algorithm'].native - encrypted_key = recipient_info['encrypted_key'].native + if cms_content["content_type"].native == "enveloped_data": + recipient_info = cms_content["content"]["recipient_infos"][0].parse() + key_enc_alg = recipient_info["key_encryption_algorithm"]["algorithm"].native + encrypted_key = recipient_info["encrypted_key"].native - if cms.KeyEncryptionAlgorithmId(key_enc_alg) == cms.KeyEncryptionAlgorithmId('rsa'): + if cms.KeyEncryptionAlgorithmId(key_enc_alg) == cms.KeyEncryptionAlgorithmId( + "rsa" + ): try: key = asymmetric.rsa_pkcs1v15_decrypt(decryption_key[0], encrypted_key) except Exception: raise DecryptionError( - 'Failed to decrypt the payload: Could not extract decryption key.') + "Failed to decrypt the payload: Could not extract decryption key." + ) - alg = cms_content['content']['encrypted_content_info']['content_encryption_algorithm'] - encapsulated_data = cms_content['content'][ - 'encrypted_content_info']['encrypted_content'].native + alg = cms_content["content"]["encrypted_content_info"][ + "content_encryption_algorithm" + ] + encapsulated_data = cms_content["content"]["encrypted_content_info"][ + "encrypted_content" + ].native try: - if alg['algorithm'].native == 'rc4': + if alg["algorithm"].native == "rc4": decrypted_content = symmetric.rc4_decrypt(key, encapsulated_data) - elif alg.encryption_cipher == 'tripledes': - cipher = 'tripledes_192_cbc' + elif alg.encryption_cipher == "tripledes": + cipher = "tripledes_192_cbc" decrypted_content = symmetric.tripledes_cbc_pkcs5_decrypt( - key, encapsulated_data, alg.encryption_iv) - elif alg.encryption_cipher == 'aes': + key, encapsulated_data, alg.encryption_iv + ) + elif alg.encryption_cipher == "aes": decrypted_content = symmetric.aes_cbc_pkcs7_decrypt( - key, encapsulated_data, alg.encryption_iv) - elif alg.encryption_cipher == 'rc2': + key, encapsulated_data, alg.encryption_iv + ) + elif alg.encryption_cipher == "rc2": decrypted_content = symmetric.rc2_cbc_pkcs5_decrypt( - key, encapsulated_data, alg['parameters']['iv'].native) + key, encapsulated_data, alg["parameters"]["iv"].native + ) else: - raise AS2Exception('Unsupported Encryption Algorithm') + raise AS2Exception("Unsupported Encryption Algorithm") except Exception as e: - raise DecryptionError('Failed to decrypt the payload: {}'.format(e)) + raise DecryptionError("Failed to decrypt the payload: {}".format(e)) else: - raise AS2Exception('Unsupported Encryption Algorithm') + raise AS2Exception("Unsupported Encryption Algorithm") else: - raise DecryptionError('Encrypted data not found in ASN.1 ') + raise DecryptionError("Encrypted data not found in ASN.1 ") return cipher, decrypted_content -def sign_message(data_to_sign, digest_alg, sign_key, - sign_alg="rsassa_pkcs1v15", use_signed_attributes=True): +def sign_message( + data_to_sign, + digest_alg, + sign_key, + sign_alg="rsassa_pkcs1v15", + use_signed_attributes=True, +): """Function signs the data and returns the generated ASN.1 :param data_to_sign: A byte string of the data to be signed. @@ -233,66 +269,106 @@ def sign_message(data_to_sign, digest_alg, sign_key, class SmimeCapability(core.Sequence): _fields = [ - ('0', core.Any, {'optional': True}), - ('1', core.Any, {'optional': True}), - ('2', core.Any, {'optional': True}), - ('3', core.Any, {'optional': True}), - ('4', core.Any, {'optional': True}) + ("0", core.Any, {"optional": True}), + ("1", core.Any, {"optional": True}), + ("2", core.Any, {"optional": True}), + ("3", core.Any, {"optional": True}), + ("4", core.Any, {"optional": True}), ] class SmimeCapabilities(core.Sequence): _fields = [ - ('0', SmimeCapability), - ('1', SmimeCapability, {'optional': True}), - ('2', SmimeCapability, {'optional': True}), - ('3', SmimeCapability, {'optional': True}), - ('4', SmimeCapability, {'optional': True}), - ('5', SmimeCapability, {'optional': True}), + ("0", SmimeCapability), + ("1", SmimeCapability, {"optional": True}), + ("2", SmimeCapability, {"optional": True}), + ("3", SmimeCapability, {"optional": True}), + ("4", SmimeCapability, {"optional": True}), + ("5", SmimeCapability, {"optional": True}), ] - smime_cap = OrderedDict([ - ('0', OrderedDict([ - ('0', core.ObjectIdentifier('2.16.840.1.101.3.4.1.42'))])), - ('1', OrderedDict([ - ('0', core.ObjectIdentifier('2.16.840.1.101.3.4.1.2'))])), - ('2', OrderedDict([ - ('0', core.ObjectIdentifier('1.2.840.113549.3.7'))])), - ('3', OrderedDict([ - ('0', core.ObjectIdentifier('1.2.840.113549.3.2')), - ('1', core.Integer(128))])), - ('4', OrderedDict([ - ('0', core.ObjectIdentifier('1.2.840.113549.3.4')), - ('1', core.Integer(128))])), - ]) - - signed_attributes = cms.CMSAttributes([ - cms.CMSAttribute({ - 'type': cms.CMSAttributeType('content_type'), - 'values': cms.SetOfContentType([ - cms.ContentType('data') - ]) - }), - cms.CMSAttribute({ - 'type': cms.CMSAttributeType('signing_time'), - 'values': cms.SetOfTime([ - cms.Time({ - 'utc_time': core.UTCTime(datetime.utcnow().replace(tzinfo=timezone.utc)) - }) - ]) - }), - cms.CMSAttribute({ - 'type': cms.CMSAttributeType('message_digest'), - 'values': cms.SetOfOctetString([ - core.OctetString(message_digest) - ]) - }), - cms.CMSAttribute({ - 'type': cms.CMSAttributeType('1.2.840.113549.1.9.15'), - 'values': cms.SetOfAny([ - core.Any(SmimeCapabilities(smime_cap)) - ]) - }), - ]) + smime_cap = OrderedDict( + [ + ( + "0", + OrderedDict( + [("0", core.ObjectIdentifier("2.16.840.1.101.3.4.1.42"))] + ), + ), + ( + "1", + OrderedDict( + [("0", core.ObjectIdentifier("2.16.840.1.101.3.4.1.2"))] + ), + ), + ( + "2", + OrderedDict([("0", core.ObjectIdentifier("1.2.840.113549.3.7"))]), + ), + ( + "3", + OrderedDict( + [ + ("0", core.ObjectIdentifier("1.2.840.113549.3.2")), + ("1", core.Integer(128)), + ] + ), + ), + ( + "4", + OrderedDict( + [ + ("0", core.ObjectIdentifier("1.2.840.113549.3.4")), + ("1", core.Integer(128)), + ] + ), + ), + ] + ) + + signed_attributes = cms.CMSAttributes( + [ + cms.CMSAttribute( + { + "type": cms.CMSAttributeType("content_type"), + "values": cms.SetOfContentType([cms.ContentType("data")]), + } + ), + cms.CMSAttribute( + { + "type": cms.CMSAttributeType("signing_time"), + "values": cms.SetOfTime( + [ + cms.Time( + { + "utc_time": core.UTCTime( + datetime.utcnow().replace( + tzinfo=timezone.utc + ) + ) + } + ) + ] + ), + } + ), + cms.CMSAttribute( + { + "type": cms.CMSAttributeType("message_digest"), + "values": cms.SetOfOctetString( + [core.OctetString(message_digest)] + ), + } + ), + cms.CMSAttribute( + { + "type": cms.CMSAttributeType("1.2.840.113549.1.9.15"), + "values": cms.SetOfAny( + [core.Any(SmimeCapabilities(smime_cap))] + ), + } + ), + ] + ) else: signed_attributes = None @@ -303,49 +379,70 @@ class SmimeCapabilities(core.Sequence): elif sign_alg == "rsassa_pss": signature = asymmetric.rsa_pss_sign(sign_key[0], data_to_sign, digest_alg) else: - raise AS2Exception('Unsupported Signature Algorithm') - - return cms.ContentInfo({ - 'content_type': cms.ContentType('signed_data'), - 'content': cms.SignedData({ - 'version': cms.CMSVersion('v1'), - 'digest_algorithms': cms.DigestAlgorithms([ - algos.DigestAlgorithm({ - 'algorithm': algos.DigestAlgorithmId(digest_alg) - }) - ]), - 'encap_content_info': cms.ContentInfo({ - 'content_type': cms.ContentType('data') - }), - 'certificates': cms.CertificateSet([ - cms.CertificateChoices({ - 'certificate': sign_key[1].asn1 - }) - ]), - 'signer_infos': cms.SignerInfos([ - cms.SignerInfo({ - 'version': cms.CMSVersion('v1'), - 'sid': cms.SignerIdentifier({ - 'issuer_and_serial_number': cms.IssuerAndSerialNumber({ - 'issuer': sign_key[1].asn1[ - 'tbs_certificate']['issuer'], - 'serial_number': sign_key[1].asn1[ - 'tbs_certificate']['serial_number'] - }) - }), - 'digest_algorithm': algos.DigestAlgorithm({ - 'algorithm': algos.DigestAlgorithmId(digest_alg) - }), - 'signed_attrs': signed_attributes, - 'signature_algorithm': algos.SignedDigestAlgorithm({ - 'algorithm': - algos.SignedDigestAlgorithmId(sign_alg) - }), - 'signature': core.OctetString(signature) - }) - ]) - }) - }).dump() + raise AS2Exception("Unsupported Signature Algorithm") + + return cms.ContentInfo( + { + "content_type": cms.ContentType("signed_data"), + "content": cms.SignedData( + { + "version": cms.CMSVersion("v1"), + "digest_algorithms": cms.DigestAlgorithms( + [ + algos.DigestAlgorithm( + {"algorithm": algos.DigestAlgorithmId(digest_alg)} + ) + ] + ), + "encap_content_info": cms.ContentInfo( + {"content_type": cms.ContentType("data")} + ), + "certificates": cms.CertificateSet( + [cms.CertificateChoices({"certificate": sign_key[1].asn1})] + ), + "signer_infos": cms.SignerInfos( + [ + cms.SignerInfo( + { + "version": cms.CMSVersion("v1"), + "sid": cms.SignerIdentifier( + { + "issuer_and_serial_number": cms.IssuerAndSerialNumber( + { + "issuer": sign_key[1].asn1[ + "tbs_certificate" + ]["issuer"], + "serial_number": sign_key[1].asn1[ + "tbs_certificate" + ]["serial_number"], + } + ) + } + ), + "digest_algorithm": algos.DigestAlgorithm( + { + "algorithm": algos.DigestAlgorithmId( + digest_alg + ) + } + ), + "signed_attrs": signed_attributes, + "signature_algorithm": algos.SignedDigestAlgorithm( + { + "algorithm": algos.SignedDigestAlgorithmId( + sign_alg + ) + } + ), + "signature": core.OctetString(signature), + } + ) + ] + ), + } + ), + } + ).dump() def verify_message(data_to_verify, signature, verify_cert): @@ -364,28 +461,28 @@ def verify_message(data_to_verify, signature, verify_cert): cms_content = cms.ContentInfo.load(signature) digest_alg = None - if cms_content['content_type'].native == 'signed_data': + if cms_content["content_type"].native == "signed_data": - for signer in cms_content['content']['signer_infos']: + for signer in cms_content["content"]["signer_infos"]: - digest_alg = signer['digest_algorithm']['algorithm'].native + digest_alg = signer["digest_algorithm"]["algorithm"].native if digest_alg not in DIGEST_ALGORITHMS: - raise Exception('Unsupported Digest Algorithm') + raise Exception("Unsupported Digest Algorithm") - sig_alg = signer['signature_algorithm']['algorithm'].native - sig = signer['signature'].native + sig_alg = signer["signature_algorithm"]["algorithm"].native + sig = signer["signature"].native signed_data = data_to_verify - if signer['signed_attrs']: + if signer["signed_attrs"]: attr_dict = {} - for attr in signer['signed_attrs']: + for attr in signer["signed_attrs"]: try: - attr_dict[attr.native['type']] = attr.native['values'] + attr_dict[attr.native["type"]] = attr.native["values"] except (ValueError, KeyError): continue message_digest = bytes() - for d in attr_dict['message_digest']: + for d in attr_dict["message_digest"]: message_digest += d digest_func = hashlib.new(digest_alg) @@ -393,23 +490,26 @@ def verify_message(data_to_verify, signature, verify_cert): calc_message_digest = digest_func.digest() if message_digest != calc_message_digest: raise IntegrityError( - 'Failed to verify message signature: Message Digest does not match.') + "Failed to verify message signature: Message Digest does not match." + ) - signed_data = signer['signed_attrs'].untag().dump() + signed_data = signer["signed_attrs"].untag().dump() try: - if sig_alg == 'rsassa_pkcs1v15': - asymmetric.rsa_pkcs1v15_verify(verify_cert, sig, signed_data, digest_alg) - elif sig_alg == 'rsassa_pss': + if sig_alg == "rsassa_pkcs1v15": + asymmetric.rsa_pkcs1v15_verify( + verify_cert, sig, signed_data, digest_alg + ) + elif sig_alg == "rsassa_pss": asymmetric.rsa_pss_verify(verify_cert, sig, signed_data, digest_alg) else: - raise AS2Exception('Unsupported Signature Algorithm') + raise AS2Exception("Unsupported Signature Algorithm") except Exception as e: import traceback + traceback.print_exc() - raise IntegrityError( - 'Failed to verify message signature: {}'.format(e)) + raise IntegrityError("Failed to verify message signature: {}".format(e)) else: - raise IntegrityError('Signed data not found in ASN.1 ') + raise IntegrityError("Signed data not found in ASN.1 ") return digest_alg diff --git a/pyas2lib/constants.py b/pyas2lib/constants.py index 0c2edd4..53e6c1f 100644 --- a/pyas2lib/constants.py +++ b/pyas2lib/constants.py @@ -1,36 +1,30 @@ """Module for defining the constants used by pyas2lib""" -AS2_VERSION = '1.2' +AS2_VERSION = "1.2" -EDIINT_FEATURES = 'CMS' +EDIINT_FEATURES = "CMS" -SYNCHRONOUS_MDN = 'SYNC' -ASYNCHRONOUS_MDN = 'ASYNC' +SYNCHRONOUS_MDN = "SYNC" +ASYNCHRONOUS_MDN = "ASYNC" -MDN_MODES = ( - SYNCHRONOUS_MDN, - ASYNCHRONOUS_MDN -) - -MDN_CONFIRM_TEXT = 'The AS2 message has been successfully processed. ' \ - 'Thank you for exchanging AS2 messages with pyAS2.' +MDN_MODES = (SYNCHRONOUS_MDN, ASYNCHRONOUS_MDN) -MDN_FAILED_TEXT = 'The AS2 message could not be processed. The ' \ - 'disposition-notification report has additional details.' +MDN_CONFIRM_TEXT = ( + "The AS2 message has been successfully processed. " + "Thank you for exchanging AS2 messages with pyAS2." +) -DIGEST_ALGORITHMS = ( - 'md5', - 'sha1', - 'sha224', - 'sha256', - 'sha384', - 'sha512' +MDN_FAILED_TEXT = ( + "The AS2 message could not be processed. The " + "disposition-notification report has additional details." ) + +DIGEST_ALGORITHMS = ("md5", "sha1", "sha224", "sha256", "sha384", "sha512") ENCRYPTION_ALGORITHMS = ( - 'tripledes_192_cbc', - 'rc2_128_cbc', - 'rc4_128_cbc', - 'aes_128_cbc', - 'aes_192_cbc', - 'aes_256_cbc', + "tripledes_192_cbc", + "rc2_128_cbc", + "rc4_128_cbc", + "aes_128_cbc", + "aes_192_cbc", + "aes_256_cbc", ) diff --git a/pyas2lib/exceptions.py b/pyas2lib/exceptions.py index 980f1a3..3edccb8 100644 --- a/pyas2lib/exceptions.py +++ b/pyas2lib/exceptions.py @@ -1,9 +1,16 @@ from __future__ import absolute_import, unicode_literals __all__ = [ - 'ImproperlyConfigured', 'AS2Exception', 'DecompressionError', - 'DecryptionError', 'InsufficientSecurityError', 'IntegrityError', - 'UnexpectedError', 'MDNNotFound', 'PartnerNotFound', 'DuplicateDocument' + "ImproperlyConfigured", + "AS2Exception", + "DecompressionError", + "DecryptionError", + "InsufficientSecurityError", + "IntegrityError", + "UnexpectedError", + "MDNNotFound", + "PartnerNotFound", + "DuplicateDocument", ] @@ -20,8 +27,8 @@ class AS2Exception(Exception): apply to :class:`~pyas2lib.ImproperlyConfigured`). """ - disposition_type = 'failed/Failure' - disposition_modifier = '' + disposition_type = "failed/Failure" + disposition_modifier = "" def __init__(self, message, disposition_modifier=None): super(AS2Exception, self).__init__(message) @@ -33,52 +40,52 @@ class PartnerNotFound(AS2Exception): """Raised when the partner/organization for the message could not be found in the system""" - disposition_type = 'processed/Error' - disposition_modifier = 'unknown-trading-partner' + disposition_type = "processed/Error" + disposition_modifier = "unknown-trading-partner" class DuplicateDocument(AS2Exception): """Raised when a message with a duplicate message ID has been received""" - disposition_type = 'processed/Warning' - disposition_modifier = 'duplicate-document' + disposition_type = "processed/Warning" + disposition_modifier = "duplicate-document" class InsufficientSecurityError(AS2Exception): """Exception raised when the message security is not as per the settings for the partner.""" - disposition_type = 'processed/Error' - disposition_modifier = 'insufficient-message-security' + disposition_type = "processed/Error" + disposition_modifier = "insufficient-message-security" class DecompressionError(AS2Exception): """Raised when the decompression process fails.""" - disposition_type = 'processed/Error' - disposition_modifier = 'decompression-failed' + disposition_type = "processed/Error" + disposition_modifier = "decompression-failed" class DecryptionError(AS2Exception): """Exception raised when decryption process fails.""" - disposition_type = 'processed/Error' - disposition_modifier = 'decryption-failed' + disposition_type = "processed/Error" + disposition_modifier = "decryption-failed" class IntegrityError(AS2Exception): """Raised when a signed message signature verification fails""" - disposition_type = 'processed/Error' - disposition_modifier = 'authentication-failed' + disposition_type = "processed/Error" + disposition_modifier = "authentication-failed" class UnexpectedError(AS2Exception): """A catch all exception to be raised for any error found while parsing a received AS2 message""" - disposition_type = 'processed/Error' - disposition_modifier = 'unexpected-processing-error' + disposition_type = "processed/Error" + disposition_modifier = "unexpected-processing-error" class MDNNotFound(AS2Exception): diff --git a/pyas2lib/tests/__init__.py b/pyas2lib/tests/__init__.py index 73e66d8..ca88db0 100644 --- a/pyas2lib/tests/__init__.py +++ b/pyas2lib/tests/__init__.py @@ -1,30 +1,28 @@ import unittest import os -TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fixtures') +TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures") class Pyas2TestCase(unittest.TestCase): - @classmethod def setUpClass(cls): """Perform the setup actions for the test case.""" file_list = { - 'test_data': 'payload.txt', - 'test_data_dos': 'payload_dos.txt', - 'private_key': 'cert_test.p12', - 'public_key': 'cert_test_public.pem', - 'mecas2_public_key': 'cert_mecas2_public.pem', - 'oldpyas2_public_key': 'cert_oldpyas2_public.pem', - 'oldpyas2_private_key': 'cert_oldpyas2_private.pem', - 'sb2bi_public_key': 'cert_sb2bi_public.pem', - 'sb2bi_public_ca': 'cert_sb2bi_public.ca', - 'private_cer': 'cert_extract_private.cer', - 'private_pem': 'cert_extract_private.pem', - + "test_data": "payload.txt", + "test_data_dos": "payload_dos.txt", + "private_key": "cert_test.p12", + "public_key": "cert_test_public.pem", + "mecas2_public_key": "cert_mecas2_public.pem", + "oldpyas2_public_key": "cert_oldpyas2_public.pem", + "oldpyas2_private_key": "cert_oldpyas2_private.pem", + "sb2bi_public_key": "cert_sb2bi_public.pem", + "sb2bi_public_ca": "cert_sb2bi_public.ca", + "private_cer": "cert_extract_private.cer", + "private_pem": "cert_extract_private.pem", } # Load the files to the attrs for attr, filename in file_list.items(): - with open(os.path.join(TEST_DIR, filename), 'rb') as fp: + with open(os.path.join(TEST_DIR, filename), "rb") as fp: setattr(cls, attr, fp.read()) diff --git a/pyas2lib/tests/livetest_with_mecas2.py b/pyas2lib/tests/livetest_with_mecas2.py index e415073..12649da 100644 --- a/pyas2lib/tests/livetest_with_mecas2.py +++ b/pyas2lib/tests/livetest_with_mecas2.py @@ -6,26 +6,25 @@ from pyas2lib import as2 from . import Pyas2TestCase -TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testdata') +TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdata") class LiveTestMecAS2(Pyas2TestCase): - def setUp(self): self.org = as2.Organization( - as2_name='pyas2lib', + as2_name="pyas2lib", sign_key=self.private_key, - sign_key_pass='test', + sign_key_pass="test", decrypt_key=self.private_key, - decrypt_key_pass='test' + decrypt_key_pass="test", ) self.partner = as2.Partner( - as2_name='mecas2', + as2_name="mecas2", verify_cert=self.mecas2_public_key, encrypt_cert=self.mecas2_public_key, mdn_mode=as2.SYNCHRONOUS_MDN, - mdn_digest_alg='sha256' + mdn_digest_alg="sha256", ) self.out_message = None @@ -37,20 +36,21 @@ def test_compressed_message(self): self.out_message.build(self.test_data) response = requests.post( - 'http://localhost:8080/as2/HttpReceiver', + "http://localhost:8080/as2/HttpReceiver", headers=self.out_message.headers, - data=self.out_message.content + data=self.out_message.content, ) - raw_mdn = '' + raw_mdn = "" for k, v in response.headers.items(): - raw_mdn += '{}: {}\n'.format(k, v) + raw_mdn += "{}: {}\n".format(k, v) - raw_mdn = raw_mdn + '\n' + response.text + raw_mdn = raw_mdn + "\n" + response.text out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - raw_mdn, find_message_cb=self.find_message) - self.assertEqual(status, 'processed') + raw_mdn, find_message_cb=self.find_message + ) + self.assertEqual(status, "processed") def test_encrypted_message(self): """ Send Encrypted Unsigned Uncompressed Message to Mendelson AS2""" @@ -60,20 +60,21 @@ def test_encrypted_message(self): self.out_message.build(self.test_data) response = requests.post( - 'http://localhost:8080/as2/HttpReceiver', + "http://localhost:8080/as2/HttpReceiver", headers=self.out_message.headers, - data=self.out_message.content + data=self.out_message.content, ) - raw_mdn = '' + raw_mdn = "" for k, v in response.headers.items(): - raw_mdn += '{}: {}\n'.format(k, v) + raw_mdn += "{}: {}\n".format(k, v) - raw_mdn = raw_mdn + '\n' + response.text + raw_mdn = raw_mdn + "\n" + response.text out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - raw_mdn, find_message_cb=self.find_message) - self.assertEqual(status, 'processed') + raw_mdn, find_message_cb=self.find_message + ) + self.assertEqual(status, "processed") def test_signed_message(self): """ Send Unencrypted Signed Uncompressed Message to Mendelson AS2""" @@ -83,20 +84,21 @@ def test_signed_message(self): self.out_message.build(self.test_data) response = requests.post( - 'http://localhost:8080/as2/HttpReceiver', + "http://localhost:8080/as2/HttpReceiver", data=self.out_message.content, headers=self.out_message.headers, ) - raw_mdn = '' + raw_mdn = "" for k, v in response.headers.items(): - raw_mdn += '{}: {}\n'.format(k, v) - raw_mdn = raw_mdn + '\n' + response.text + raw_mdn += "{}: {}\n".format(k, v) + raw_mdn = raw_mdn + "\n" + response.text out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - raw_mdn, find_message_cb=self.find_message) - self.assertEqual(status, 'processed') + raw_mdn, find_message_cb=self.find_message + ) + self.assertEqual(status, "processed") def test_encrypted_signed_message(self): """ Send Encrypted Signed Uncompressed Message to Mendelson AS2""" @@ -107,20 +109,21 @@ def test_encrypted_signed_message(self): self.out_message.build(self.test_data) response = requests.post( - 'http://localhost:8080/as2/HttpReceiver', + "http://localhost:8080/as2/HttpReceiver", data=self.out_message.content, headers=self.out_message.headers, ) - raw_mdn = '' + raw_mdn = "" for k, v in response.headers.items(): - raw_mdn += '{}: {}\n'.format(k, v) - raw_mdn = raw_mdn + '\n' + response.text + raw_mdn += "{}: {}\n".format(k, v) + raw_mdn = raw_mdn + "\n" + response.text out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - raw_mdn, find_message_cb=self.find_message) - self.assertEqual(status, 'processed') + raw_mdn, find_message_cb=self.find_message + ) + self.assertEqual(status, "processed") def test_encrypted_signed_compressed_message(self): """ Send Encrypted Signed Compressed Message to Mendelson AS2""" @@ -132,20 +135,21 @@ def test_encrypted_signed_compressed_message(self): self.out_message.build(self.test_data) response = requests.post( - 'http://localhost:8080/as2/HttpReceiver', + "http://localhost:8080/as2/HttpReceiver", data=self.out_message.content, headers=self.out_message.headers, ) - raw_mdn = '' + raw_mdn = "" for k, v in response.headers.items(): - raw_mdn += '{}: {}\n'.format(k, v) - raw_mdn = raw_mdn + '\n' + response.text + raw_mdn += "{}: {}\n".format(k, v) + raw_mdn = raw_mdn + "\n" + response.text out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - raw_mdn, find_message_cb=self.find_message) - self.assertEqual(status, 'processed') + raw_mdn, find_message_cb=self.find_message + ) + self.assertEqual(status, "processed") def find_org(self, headers): return self.org diff --git a/pyas2lib/tests/livetest_with_oldpyas2.py b/pyas2lib/tests/livetest_with_oldpyas2.py index 016e976..6b30c49 100644 --- a/pyas2lib/tests/livetest_with_oldpyas2.py +++ b/pyas2lib/tests/livetest_with_oldpyas2.py @@ -6,26 +6,25 @@ from pyas2lib import as2 from . import Pyas2TestCase -TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testdata') +TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdata") class LiveTestMecAS2(Pyas2TestCase): - def setUp(self): self.org = as2.Organization( - as2_name='pyas2lib', + as2_name="pyas2lib", sign_key=self.private_key, - sign_key_pass='test', + sign_key_pass="test", decrypt_key=self.private_key, - decrypt_key_pass='test' + decrypt_key_pass="test", ) self.partner = as2.Partner( - as2_name='pyas2idev', + as2_name="pyas2idev", verify_cert=self.oldpyas2_public_key, encrypt_cert=self.oldpyas2_public_key, mdn_mode=as2.SYNCHRONOUS_MDN, - mdn_digest_alg='sha256' + mdn_digest_alg="sha256", ) self.out_message = None @@ -37,20 +36,21 @@ def test_compressed_message(self): self.out_message.build(self.test_data) response = requests.post( - 'http://localhost:8080/pyas2/as2receive', + "http://localhost:8080/pyas2/as2receive", headers=self.out_message.headers, - data=self.out_message.content + data=self.out_message.content, ) - raw_mdn = '' + raw_mdn = "" for k, v in response.headers.items(): - raw_mdn += '{}: {}\n'.format(k, v) + raw_mdn += "{}: {}\n".format(k, v) - raw_mdn = raw_mdn + '\n' + response.text + raw_mdn = raw_mdn + "\n" + response.text out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - raw_mdn, find_message_cb=self.find_message) - self.assertEqual(status, 'processed') + raw_mdn, find_message_cb=self.find_message + ) + self.assertEqual(status, "processed") def test_encrypted_message(self): """ Send Encrypted Unsigned Uncompressed Message to Mendelson AS2""" @@ -60,20 +60,21 @@ def test_encrypted_message(self): self.out_message.build(self.test_data) response = requests.post( - 'http://localhost:8080/pyas2/as2receive', + "http://localhost:8080/pyas2/as2receive", headers=self.out_message.headers, - data=self.out_message.content + data=self.out_message.content, ) - raw_mdn = '' + raw_mdn = "" for k, v in response.headers.items(): - raw_mdn += '{}: {}\n'.format(k, v) + raw_mdn += "{}: {}\n".format(k, v) - raw_mdn = raw_mdn + '\n' + response.text + raw_mdn = raw_mdn + "\n" + response.text out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - raw_mdn, find_message_cb=self.find_message) - self.assertEqual(status, 'processed') + raw_mdn, find_message_cb=self.find_message + ) + self.assertEqual(status, "processed") def test_signed_message(self): """ Send Unencrypted Signed Uncompressed Message to Mendelson AS2""" @@ -83,20 +84,21 @@ def test_signed_message(self): self.out_message.build(self.test_data) response = requests.post( - 'http://localhost:8080/pyas2/as2receive', + "http://localhost:8080/pyas2/as2receive", data=self.out_message.content, headers=self.out_message.headers, ) - raw_mdn = '' + raw_mdn = "" for k, v in response.headers.items(): - raw_mdn += '{}: {}\n'.format(k, v) - raw_mdn = raw_mdn + '\n' + response.text + raw_mdn += "{}: {}\n".format(k, v) + raw_mdn = raw_mdn + "\n" + response.text out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - raw_mdn, find_message_cb=self.find_message) - self.assertEqual(status, 'processed') + raw_mdn, find_message_cb=self.find_message + ) + self.assertEqual(status, "processed") def test_encrypted_signed_message(self): """ Send Encrypted Signed Uncompressed Message to Mendelson AS2""" @@ -107,20 +109,21 @@ def test_encrypted_signed_message(self): self.out_message.build(self.test_data) response = requests.post( - 'http://localhost:8080/pyas2/as2receive', + "http://localhost:8080/pyas2/as2receive", data=self.out_message.content, headers=self.out_message.headers, ) - raw_mdn = '' + raw_mdn = "" for k, v in response.headers.items(): - raw_mdn += '{}: {}\n'.format(k, v) - raw_mdn = raw_mdn + '\n' + response.text + raw_mdn += "{}: {}\n".format(k, v) + raw_mdn = raw_mdn + "\n" + response.text out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - raw_mdn, find_message_cb=self.find_message) - self.assertEqual(status, 'processed') + raw_mdn, find_message_cb=self.find_message + ) + self.assertEqual(status, "processed") def test_encrypted_signed_compressed_message(self): """ Send Encrypted Signed Compressed Message to Mendelson AS2""" @@ -132,20 +135,21 @@ def test_encrypted_signed_compressed_message(self): self.out_message.build(self.test_data) response = requests.post( - 'http://localhost:8080/pyas2/as2receive', + "http://localhost:8080/pyas2/as2receive", data=self.out_message.content, headers=self.out_message.headers, ) - raw_mdn = '' + raw_mdn = "" for k, v in response.headers.items(): - raw_mdn += '{}: {}\n'.format(k, v) - raw_mdn = raw_mdn + '\n' + response.text + raw_mdn += "{}: {}\n".format(k, v) + raw_mdn = raw_mdn + "\n" + response.text out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - raw_mdn, find_message_cb=self.find_message) - self.assertEqual(status, 'processed') + raw_mdn, find_message_cb=self.find_message + ) + self.assertEqual(status, "processed") def find_org(self, headers): return self.org diff --git a/pyas2lib/tests/test_advanced.py b/pyas2lib/tests/test_advanced.py index 185e962..5179513 100644 --- a/pyas2lib/tests/test_advanced.py +++ b/pyas2lib/tests/test_advanced.py @@ -10,17 +10,16 @@ class TestAdvanced(Pyas2TestCase): - def setUp(self): self.org = as2.Organization( - as2_name='some_organization', + as2_name="some_organization", sign_key=self.private_key, - sign_key_pass='test', + sign_key_pass="test", decrypt_key=self.private_key, - decrypt_key_pass='test' + decrypt_key_pass="test", ) self.partner = as2.Partner( - as2_name='some_partner', + as2_name="some_partner", verify_cert=self.public_key, encrypt_cert=self.public_key, ) @@ -33,15 +32,15 @@ def test_binary_message(self): self.partner.encrypt = True self.partner.compress = True out_message = as2.Message(self.org, self.partner) - test_message_path = os.path.join(TEST_DIR, 'payload.binary') - with open(test_message_path, 'rb') as bin_file: + test_message_path = os.path.join(TEST_DIR, "payload.binary") + with open(test_message_path, "rb") as bin_file: original_message = bin_file.read() out_message.build( original_message, - filename='payload.binary', - content_type='application/octet-stream' + filename="payload.binary", + content_type="application/octet-stream", ) - raw_out_message = out_message.headers_str + b'\r\n' + out_message.content + raw_out_message = out_message.headers_str + b"\r\n" + out_message.content # Parse the generated AS2 message as the partner in_message = as2.Message() @@ -49,13 +48,13 @@ def test_binary_message(self): raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner, - find_message_cb=lambda x, y: False + find_message_cb=lambda x, y: False, ) # Compare the mic contents of the input and output messages # self.assertEqual(original_message, # in_message.payload.get_payload(decode=True)) - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertEqual(out_message.mic, in_message.mic) @@ -71,24 +70,24 @@ def test_partner_not_found(self): self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) in_message = as2.Message() _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=lambda x: None, - find_message_cb=lambda x, y: False + find_message_cb=lambda x, y: False, ) out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'processed/Error') - self.assertEqual(detailed_status, 'unknown-trading-partner') + self.assertEqual(status, "processed/Error") + self.assertEqual(detailed_status, "unknown-trading-partner") # Parse again but this time make without organization in_message = as2.Message() @@ -96,16 +95,15 @@ def test_partner_not_found(self): raw_out_message, find_org_cb=lambda x: None, find_partner_cb=self.find_partner, - find_message_cb=lambda x, y: False + find_message_cb=lambda x, y: False, ) out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'processed/Error') - self.assertEqual(detailed_status, 'unknown-trading-partner') + self.assertEqual(status, "processed/Error") + self.assertEqual(detailed_status, "unknown-trading-partner") def test_duplicate_message(self): """ Test case where a duplicate message is sent to the partner """ @@ -118,23 +116,23 @@ def test_duplicate_message(self): self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) in_message = as2.Message() _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner, - find_message_cb=lambda x, y: True + find_message_cb=lambda x, y: True, ) out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'processed/Warning') - self.assertEqual(detailed_status, 'duplicate-document') + self.assertEqual(status, "processed/Warning") + self.assertEqual(detailed_status, "duplicate-document") def test_failed_decompression(self): """ Test case where message decompression has failed """ @@ -146,8 +144,9 @@ def test_failed_decompression(self): self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + base64.b64encode(b'xxxxx') + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + base64.b64encode(b"xxxxx") + ) in_message = as2.Message() _, exec_info, mdn = in_message.parse( raw_out_message, @@ -157,11 +156,10 @@ def test_failed_decompression(self): out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'processed/Error') - self.assertEqual(detailed_status, 'decompression-failed') + self.assertEqual(status, "processed/Error") + self.assertEqual(detailed_status, "decompression-failed") def test_insufficient_security(self): """ Test case where message security is not as per the configuration """ @@ -173,17 +171,18 @@ def test_insufficient_security(self): # Parse the generated AS2 message as the partner self.partner.encrypt = True - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) in_message = as2.Message() status, (exc, _), mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner, - find_message_cb=lambda x, y: False + find_message_cb=lambda x, y: False, ) - self.assertEqual(status, 'processed/Error') - self.assertEqual(exc.disposition_modifier, 'insufficient-message-security') + self.assertEqual(status, "processed/Error") + self.assertEqual(exc.disposition_modifier, "insufficient-message-security") # Try again for signing check self.partner.encrypt = False @@ -193,10 +192,10 @@ def test_insufficient_security(self): raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner, - find_message_cb=lambda x, y: False + find_message_cb=lambda x, y: False, ) - self.assertEqual(status, 'processed/Error') - self.assertEqual(exc.disposition_modifier, 'insufficient-message-security') + self.assertEqual(status, "processed/Error") + self.assertEqual(exc.disposition_modifier, "insufficient-message-security") def test_failed_decryption(self): """ Test case where message decryption has failed """ @@ -210,23 +209,23 @@ def test_failed_decryption(self): self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) in_message = as2.Message() _, exec_info, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner, - find_message_cb=lambda x, y: False + find_message_cb=lambda x, y: False, ) out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'processed/Error') - self.assertEqual(detailed_status, 'decryption-failed') + self.assertEqual(status, "processed/Error") + self.assertEqual(detailed_status, "decryption-failed") def test_failed_signature(self): """ Test case where signature verification has failed """ @@ -240,153 +239,137 @@ def test_failed_signature(self): self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) in_message = as2.Message() _, exec_info, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner, - find_message_cb=lambda x, y: False + find_message_cb=lambda x, y: False, ) out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'processed/Error') - self.assertEqual(detailed_status, 'authentication-failed') + self.assertEqual(status, "processed/Error") + self.assertEqual(detailed_status, "authentication-failed") def test_verify_certificate(self): """ Test case where we have try to load an expired cert """ # First test with a certificate with invalid root - cert_path = os.path.join(TEST_DIR, 'verify_cert_test1.pem') - with open(cert_path, 'rb') as cert_file: + cert_path = os.path.join(TEST_DIR, "verify_cert_test1.pem") + with open(cert_path, "rb") as cert_file: try: - as2.Partner( - as2_name='some_partner', - verify_cert=cert_file.read() - ) + as2.Partner(as2_name="some_partner", verify_cert=cert_file.read()) except as2.AS2Exception as e: - self.assertIn( - 'unable to get local issuer certificate', str(e)) + self.assertIn("unable to get local issuer certificate", str(e)) # Test with an expired certificate - cert_path = os.path.join(TEST_DIR, 'verify_cert_test2.cer') - with open(cert_path, 'rb') as cert_file: + cert_path = os.path.join(TEST_DIR, "verify_cert_test2.cer") + with open(cert_path, "rb") as cert_file: try: - as2.Partner( - as2_name='some_partner', - verify_cert=cert_file.read() - ) + as2.Partner(as2_name="some_partner", verify_cert=cert_file.read()) except as2.AS2Exception as e: - self.assertIn( - 'certificate has expired', str(e)) + self.assertIn("certificate has expired", str(e)) # Test with a chain certificate - cert_path = os.path.join(TEST_DIR, 'verify_cert_test3.pem') - with open(cert_path, 'rb') as cert_file: + cert_path = os.path.join(TEST_DIR, "verify_cert_test3.pem") + with open(cert_path, "rb") as cert_file: try: - as2.Partner( - as2_name='some_partner', - verify_cert=cert_file.read() - ) + as2.Partner(as2_name="some_partner", verify_cert=cert_file.read()) except as2.AS2Exception as e: - self.assertIn( - 'unable to get local issuer certificate', str(e)) + self.assertIn("unable to get local issuer certificate", str(e)) # Test chain certificate with the ca - cert_ca_path = os.path.join(TEST_DIR, 'verify_cert_test3.ca') - with open(cert_path, 'rb') as cert_file: - with open(cert_ca_path, 'rb') as cert_ca_file: + cert_ca_path = os.path.join(TEST_DIR, "verify_cert_test3.ca") + with open(cert_path, "rb") as cert_file: + with open(cert_ca_path, "rb") as cert_ca_file: try: as2.Partner( - as2_name='some_partner', + as2_name="some_partner", verify_cert=cert_file.read(), - verify_cert_ca=cert_ca_file.read() + verify_cert_ca=cert_ca_file.read(), ) except as2.AS2Exception as e: - self.fail('Failed to load chain certificate: %s' % e) + self.fail("Failed to load chain certificate: %s" % e) def test_load_private_key(self): """ Test case where we have try to load keys in different formats """ # First test with a pkcs12 key file - cert_path = os.path.join(TEST_DIR, 'cert_test.p12') - with open(cert_path, 'rb') as cert_file: + cert_path = os.path.join(TEST_DIR, "cert_test.p12") + with open(cert_path, "rb") as cert_file: try: as2.Organization( - as2_name='some_org', - sign_key=cert_file.read(), - sign_key_pass='test' + as2_name="some_org", sign_key=cert_file.read(), sign_key_pass="test" ) except as2.AS2Exception as e: - self.fail('Failed to load p12 private key: %s' % e) + self.fail("Failed to load p12 private key: %s" % e) # Now test with a pem encoded key file - cert_path = os.path.join(TEST_DIR, 'cert_test.pem') - with open(cert_path, 'rb') as cert_file: + cert_path = os.path.join(TEST_DIR, "cert_test.pem") + with open(cert_path, "rb") as cert_file: try: as2.Organization( - as2_name='some_org', - sign_key=cert_file.read(), - sign_key_pass='test' + as2_name="some_org", sign_key=cert_file.read(), sign_key_pass="test" ) except as2.AS2Exception as e: - self.fail('Failed to load pem private key: %s' % e) + self.fail("Failed to load pem private key: %s" % e) def test_partner_checks(self): """Test the checks for the partner on initialization.""" with self.assertRaises(ImproperlyConfigured): - as2.Partner('a partner', digest_alg='xyz') + as2.Partner("a partner", digest_alg="xyz") with self.assertRaises(ImproperlyConfigured): - as2.Partner('a partner', enc_alg='xyz') + as2.Partner("a partner", enc_alg="xyz") with self.assertRaises(ImproperlyConfigured): - as2.Partner('a partner', mdn_mode='xyz') + as2.Partner("a partner", mdn_mode="xyz") with self.assertRaises(ImproperlyConfigured): - as2.Partner('a partner', mdn_digest_alg='xyz') + as2.Partner("a partner", mdn_digest_alg="xyz") def test_message_checks(self): """Test the checks and other features of Message.""" msg = as2.Message() - assert msg.content == '' + assert msg.content == "" assert msg.headers == {} - assert msg.headers_str == b'' + assert msg.headers_str == b"" msg.payload = message.Message() - msg.payload.set_payload(b'data') - assert msg.content == b'data' + msg.payload.set_payload(b"data") + assert msg.content == b"data" - org = as2.Organization(as2_name='AS2 Server') - partner = as2.Partner(as2_name='AS2 Partner', sign=True) + org = as2.Organization(as2_name="AS2 Server") + partner = as2.Partner(as2_name="AS2 Partner", sign=True) msg = as2.Message(sender=org, receiver=partner) with self.assertRaises(ImproperlyConfigured): - msg.build(b'data') + msg.build(b"data") msg.receiver.sign = False msg.receiver.encrypt = True with self.assertRaises(ImproperlyConfigured): - msg.build(b'data') + msg.build(b"data") msg.receiver.encrypt = False - msg.receiver.mdn_mode = 'ASYNC' + msg.receiver.mdn_mode = "ASYNC" with self.assertRaises(ImproperlyConfigured): - msg.build(b'data') + msg.build(b"data") - msg.sender.mdn_url = 'http://localhost/pyas2/as2receive' - msg.build(b'data') + msg.sender.mdn_url = "http://localhost/pyas2/as2receive" + msg.build(b"data") def test_mdn_checks(self): """Test the checks and other features of MDN.""" mdn = as2.Mdn() - assert mdn.content == '' + assert mdn.content == "" assert mdn.headers == {} - assert mdn.headers_str == b'' + assert mdn.headers_str == b"" def test_mdn_not_found(self): """Test that the MDN parser raises MDN not found when a non MDN message is passed.""" @@ -398,10 +381,11 @@ def test_mdn_not_found(self): # Parse the AS2 message as an MDN mdn = as2.Mdn() - raw_out_message = self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) status, detailed_status = mdn.parse( - raw_out_message, - find_message_cb=self.find_message + raw_out_message, find_message_cb=self.find_message ) self.assertEqual(status, "failed/Failure") self.assertEqual(detailed_status, "mdn-not-found") @@ -413,26 +397,28 @@ def test_unsigned_mdn_sent_error(self): self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) in_message = as2.Message() _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner, - find_message_cb=lambda x, y: False + find_message_cb=lambda x, y: False, ) # Set the mdn sig alg and parse it self.partner.mdn_digest_alg = "sha256" out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'failed/Failure') - self.assertEqual(detailed_status, 'Expected signed MDN but unsigned MDN returned') + self.assertEqual(status, "failed/Failure") + self.assertEqual( + detailed_status, "Expected signed MDN but unsigned MDN returned" + ) def test_non_matching_mic(self): """Test the case where a the mic in the mdn does not match the mic in the message.""" @@ -442,26 +428,26 @@ def test_non_matching_mic(self): self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) in_message = as2.Message() _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, find_partner_cb=self.find_partner, - find_message_cb=lambda x, y: False + find_message_cb=lambda x, y: False, ) # Set the mdn sig alg and parse it self.out_message.mic = b"dummy value" out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'processed/warning') - self.assertEqual(detailed_status, 'Message Integrity check failed.') + self.assertEqual(status, "processed/warning") + self.assertEqual(detailed_status, "Message Integrity check failed.") def find_org(self, headers): return self.org @@ -474,17 +460,16 @@ def find_message(self, message_id, message_recipient): class SterlingIntegratorTest(Pyas2TestCase): - def setUp(self): self.org = as2.Organization( - as2_name='AS2 Server', + as2_name="AS2 Server", sign_key=self.oldpyas2_private_key, - sign_key_pass='password', + sign_key_pass="password", decrypt_key=self.oldpyas2_private_key, - decrypt_key_pass='password' + decrypt_key_pass="password", ) self.partner = as2.Partner( - as2_name='Sterling B2B Integrator', + as2_name="Sterling B2B Integrator", verify_cert=self.sb2bi_public_key, verify_cert_ca=self.sb2bi_public_ca, encrypt_cert=self.sb2bi_public_key, @@ -496,23 +481,25 @@ def setUp(self): @pytest.mark.skip(reason="no way of currently testing this") def test_process_message(self): """ Test processing message received from Sterling Integrator""" - with open(os.path.join(TEST_DIR, 'sb2bi_signed_cmp.msg'), 'rb') as msg: + with open(os.path.join(TEST_DIR, "sb2bi_signed_cmp.msg"), "rb") as msg: as2message = as2.Message() status, exception, as2mdn = as2message.parse( msg.read(), lambda x: self.org, lambda y: self.partner, - lambda x, y: False + lambda x, y: False, ) - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") def test_process_mdn(self): """ Test processing mdn received from Sterling Integrator""" msg = as2.Message(sender=self.org, receiver=self.partner) - msg.message_id = '151694007918.24690.7052273208458909245@ip-172-31-14-209.ec2.internal' + msg.message_id = ( + "151694007918.24690.7052273208458909245@ip-172-31-14-209.ec2.internal" + ) as2mdn = as2.Mdn() # Parse the mdn and get the message status - with open(os.path.join(TEST_DIR, 'sb2bi_signed.mdn'), 'rb') as mdn: + with open(os.path.join(TEST_DIR, "sb2bi_signed.mdn"), "rb") as mdn: status, detailed_status = as2mdn.parse(mdn.read(), lambda x, y: msg) - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") diff --git a/pyas2lib/tests/test_basic.py b/pyas2lib/tests/test_basic.py index 38fc540..fc6c20a 100644 --- a/pyas2lib/tests/test_basic.py +++ b/pyas2lib/tests/test_basic.py @@ -5,19 +5,18 @@ class TestBasic(Pyas2TestCase): - def setUp(self): self.org = as2.Organization( - as2_name='some_organization', + as2_name="some_organization", sign_key=self.private_key, - sign_key_pass='test', + sign_key_pass="test", decrypt_key=self.private_key, - decrypt_key_pass='test' + decrypt_key_pass="test", ) self.partner = as2.Partner( - as2_name='some_partner', + as2_name="some_partner", verify_cert=self.public_key, - encrypt_cert=self.public_key + encrypt_cert=self.public_key, ) def test_plain_message(self): @@ -26,19 +25,18 @@ def test_plain_message(self): # Build an As2 message to be transmitted to partner out_message = as2.Message(self.org, self.partner) out_message.build(self.test_data) - raw_out_message = \ - out_message.headers_str + b'\r\n' + out_message.content + raw_out_message = out_message.headers_str + b"\r\n" + out_message.content # Parse the generated AS2 message as the partner in_message = as2.Message() status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, ) # Compare contents of the input and output messages - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") self.assertEqual(self.test_data, in_message.content) def test_compressed_message(self): @@ -48,18 +46,18 @@ def test_compressed_message(self): self.partner.compress = True out_message = as2.Message(self.org, self.partner) out_message.build(self.test_data) - raw_out_message = out_message.headers_str + b'\r\n' + out_message.content + raw_out_message = out_message.headers_str + b"\r\n" + out_message.content # Parse the generated AS2 message as the partner in_message = as2.Message() status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, ) # Compare the mic contents of the input and output messages - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") self.assertTrue(in_message.compressed) self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) @@ -70,18 +68,18 @@ def test_encrypted_message(self): self.partner.encrypt = True out_message = as2.Message(self.org, self.partner) out_message.build(self.test_data) - raw_out_message = out_message.headers_str + b'\r\n' + out_message.content + raw_out_message = out_message.headers_str + b"\r\n" + out_message.content # Parse the generated AS2 message as the partner in_message = as2.Message() status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, ) # Compare the mic contents of the input and output messages - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") self.assertTrue(in_message.encrypted) self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) @@ -92,18 +90,18 @@ def test_signed_message(self): self.partner.sign = True out_message = as2.Message(self.org, self.partner) out_message.build(self.test_data) - raw_out_message = out_message.headers_str + b'\r\n' + out_message.content + raw_out_message = out_message.headers_str + b"\r\n" + out_message.content # Parse the generated AS2 message as the partner in_message = as2.Message() status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, ) # Compare the mic contents of the input and output messages - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) self.assertTrue(in_message.signed) self.assertEqual(out_message.mic, in_message.mic) @@ -116,18 +114,18 @@ def test_encrypted_signed_message(self): self.partner.encrypt = True out_message = as2.Message(self.org, self.partner) out_message.build(self.test_data) - raw_out_message = out_message.headers_str + b'\r\n' + out_message.content + raw_out_message = out_message.headers_str + b"\r\n" + out_message.content # Parse the generated AS2 message as the partner in_message = as2.Message() status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, ) # Compare the mic contents of the input and output messages - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertEqual(out_message.mic, in_message.mic) @@ -141,18 +139,18 @@ def test_encrypted_signed_message_dos(self): self.partner.encrypt = True out_message = as2.Message(self.org, self.partner) out_message.build(self.test_data_dos) - raw_out_message = out_message.headers_str + b'\r\n' + out_message.content + raw_out_message = out_message.headers_str + b"\r\n" + out_message.content # Parse the generated AS2 message as the partner in_message = as2.Message() status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, ) # Compare the mic contents of the input and output messages - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertEqual(out_message.mic, in_message.mic) @@ -167,18 +165,18 @@ def test_encrypted_signed_compressed_message(self): self.partner.compress = True out_message = as2.Message(self.org, self.partner) out_message.build(self.test_data) - raw_out_message = out_message.headers_str + b'\r\n' + out_message.content + raw_out_message = out_message.headers_str + b"\r\n" + out_message.content # Parse the generated AS2 message as the partner in_message = as2.Message() status, _, _ = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, ) # Compare the mic contents of the input and output messages - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertTrue(in_message.compressed) diff --git a/pyas2lib/tests/test_cms.py b/pyas2lib/tests/test_cms.py index 23d0069..9494bfd 100644 --- a/pyas2lib/tests/test_cms.py +++ b/pyas2lib/tests/test_cms.py @@ -10,20 +10,20 @@ AS2Exception, DecompressionError, DecryptionError, - IntegrityError + IntegrityError, ) from pyas2lib.tests import TEST_DIR -INVALID_DATA = cms.cms.ContentInfo({ - 'content_type': cms.cms.ContentType('data'), -}).dump() +INVALID_DATA = cms.cms.ContentInfo( + {"content_type": cms.cms.ContentType("data"),} +).dump() def test_compress(): """Test the compression and decompression functions.""" - compressed_data = cms.compress_message(b'data') - assert cms.decompress_message(compressed_data) == b'data' + compressed_data = cms.compress_message(b"data") + assert cms.decompress_message(compressed_data) == b"data" with pytest.raises(DecompressionError): cms.decompress_message(INVALID_DATA) @@ -32,41 +32,51 @@ def test_compress(): def test_signing(): """Test the signing and verification functions.""" # Load the signature key - with open(os.path.join(TEST_DIR, "cert_test.p12"), 'rb') as fp: + with open(os.path.join(TEST_DIR, "cert_test.p12"), "rb") as fp: sign_key = Organization.load_key(fp.read(), "test") - with open(os.path.join(TEST_DIR, "cert_test_public.pem"), 'rb') as fp: + with open(os.path.join(TEST_DIR, "cert_test_public.pem"), "rb") as fp: verify_cert = asymmetric.load_certificate(fp.read()) # Test failure of signature verification with pytest.raises(IntegrityError): - cms.verify_message(b'data', INVALID_DATA, None) + cms.verify_message(b"data", INVALID_DATA, None) # Test signature without signed attributes - cms.sign_message(b"data", digest_alg="sha256", sign_key=sign_key, use_signed_attributes=False) + cms.sign_message( + b"data", digest_alg="sha256", sign_key=sign_key, use_signed_attributes=False + ) # Test pss signature and verification signature = cms.sign_message( - b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pss") + b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pss" + ) cms.verify_message(b"data", signature, verify_cert) # Test unsupported signature alg with pytest.raises(AS2Exception): cms.sign_message( - b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pssa") + b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pssa" + ) def test_encryption(): """Test the encryption and decryption functions.""" - with open(os.path.join(TEST_DIR, "cert_test.p12"), 'rb') as fp: + with open(os.path.join(TEST_DIR, "cert_test.p12"), "rb") as fp: decrypt_key = Organization.load_key(fp.read(), "test") - with open(os.path.join(TEST_DIR, "cert_test_public.pem"), 'rb') as fp: + with open(os.path.join(TEST_DIR, "cert_test_public.pem"), "rb") as fp: encrypt_cert = asymmetric.load_certificate(fp.read()) with pytest.raises(DecryptionError): cms.decrypt_message(INVALID_DATA, None) # Test all the encryption algorithms - enc_algorithms = ['rc2_128_cbc', 'rc4_128_cbc', 'aes_128_cbc', 'aes_192_cbc', 'aes_256_cbc'] + enc_algorithms = [ + "rc2_128_cbc", + "rc4_128_cbc", + "aes_128_cbc", + "aes_192_cbc", + "aes_256_cbc", + ] for enc_algorithm in enc_algorithms: encrypted_data = cms.encrypt_message(b"data", enc_algorithm, encrypt_cert) _, decrypted_data = cms.decrypt_message(encrypted_data, decrypt_key) diff --git a/pyas2lib/tests/test_mdn.py b/pyas2lib/tests/test_mdn.py index 9787841..59ebfcc 100644 --- a/pyas2lib/tests/test_mdn.py +++ b/pyas2lib/tests/test_mdn.py @@ -4,18 +4,17 @@ class TestMDN(Pyas2TestCase): - def setUp(self): self.org = as2.Organization( - as2_name='some_organization', + as2_name="some_organization", sign_key=self.private_key, - sign_key_pass='test', + sign_key_pass="test", decrypt_key=self.private_key, - decrypt_key_pass='test' + decrypt_key_pass="test", ) self.partner = as2.Partner( - as2_name='some_partner', + as2_name="some_partner", verify_cert=self.public_key, encrypt_cert=self.public_key, ) @@ -32,22 +31,22 @@ def test_unsigned_mdn(self): self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) in_message = as2.Message() _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, ) out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") def test_signed_mdn(self): """ Test signed MDN generation and parsing """ @@ -56,26 +55,26 @@ def test_signed_mdn(self): self.partner.sign = True self.partner.encrypt = True self.partner.mdn_mode = as2.SYNCHRONOUS_MDN - self.partner.mdn_digest_alg = 'sha256' + self.partner.mdn_digest_alg = "sha256" self.out_message = as2.Message(self.org, self.partner) self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) in_message = as2.Message() _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, ) out_mdn = as2.Mdn() status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'processed') + self.assertEqual(status, "processed") def test_failed_mdn_parse(self): """Test mdn parsing failures are captured.""" @@ -83,31 +82,33 @@ def test_failed_mdn_parse(self): self.partner.sign = True self.partner.encrypt = True self.partner.mdn_mode = as2.SYNCHRONOUS_MDN - self.partner.mdn_digest_alg = 'sha256' + self.partner.mdn_digest_alg = "sha256" self.out_message = as2.Message(self.org, self.partner) self.out_message.build(self.test_data) # Parse the generated AS2 message as the partner - raw_out_message = \ - self.out_message.headers_str + b'\r\n' + self.out_message.content + raw_out_message = ( + self.out_message.headers_str + b"\r\n" + self.out_message.content + ) in_message = as2.Message() _, _, mdn = in_message.parse( raw_out_message, find_org_cb=self.find_org, - find_partner_cb=self.find_partner + find_partner_cb=self.find_partner, ) out_mdn = as2.Mdn() self.partner.verify_cert = self.mecas2_public_key self.partner.validate_certs = False status, detailed_status = out_mdn.parse( - mdn.headers_str + b'\r\n' + mdn.content, - find_message_cb=self.find_message + mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message ) - self.assertEqual(status, 'failed/Failure') + self.assertEqual(status, "failed/Failure") self.assertEqual( - detailed_status, 'Failed to parse received MDN. Failed to verify message signature: ' - 'Message Digest does not match.') + detailed_status, + "Failed to parse received MDN. Failed to verify message signature: " + "Message Digest does not match.", + ) def find_org(self, as2_id): return self.org diff --git a/pyas2lib/tests/test_utils.py b/pyas2lib/tests/test_utils.py index 8222af2..23b31d2 100644 --- a/pyas2lib/tests/test_utils.py +++ b/pyas2lib/tests/test_utils.py @@ -11,40 +11,43 @@ def test_quoting(): """Test the function for quoting and as2 name.""" - assert utils.quote_as2name('PYAS2LIB') == 'PYAS2LIB' - assert utils.quote_as2name('PYAS2 LIB') == '"PYAS2 LIB"' + assert utils.quote_as2name("PYAS2LIB") == "PYAS2LIB" + assert utils.quote_as2name("PYAS2 LIB") == '"PYAS2 LIB"' def test_bytes_generator(): """Test the email bytes generator class.""" message = Message() - message.set_type('application/pkcs7-mime') - assert utils.mime_to_bytes(message) == b'MIME-Version: 1.0\r\n' \ - b'Content-Type: application/pkcs7-mime\r\n\r\n' + message.set_type("application/pkcs7-mime") + assert ( + utils.mime_to_bytes(message) == b"MIME-Version: 1.0\r\n" + b"Content-Type: application/pkcs7-mime\r\n\r\n" + ) def test_make_boundary(): """Test the function for creating a boundary for multipart messages.""" - assert utils.make_mime_boundary(text='123456') is not None + assert utils.make_mime_boundary(text="123456") is not None def test_extract_first_part(): """Test the function for extracting the first part of a multipart message.""" - message = b'header----first_part\n----second_part\n' - assert utils.extract_first_part(message, b'----') == b'first_part' + message = b"header----first_part\n----second_part\n" + assert utils.extract_first_part(message, b"----") == b"first_part" - message = b'header----first_part\r\n----second_part\r\n' - assert utils.extract_first_part(message, b'----') == b'first_part' + message = b"header----first_part\r\n----second_part\r\n" + assert utils.extract_first_part(message, b"----") == b"first_part" def test_cert_verification(): """Test the verification of a certificate chain.""" - with open(os.path.join(TEST_DIR, 'cert_sb2bi_public.pem'), 'rb') as fp: + with open(os.path.join(TEST_DIR, "cert_sb2bi_public.pem"), "rb") as fp: certificate = utils.pem_to_der(fp.read(), return_multiple=False) with pytest.raises(AS2Exception): utils.verify_certificate_chain( - certificate, trusted_certs=[], ignore_self_signed=False) + certificate, trusted_certs=[], ignore_self_signed=False + ) def test_extract_certificate_info(): @@ -52,31 +55,45 @@ def test_extract_certificate_info(): in PEM or DER format""" cert_info = { - 'valid_from': datetime.datetime(2019, 6, 3, 11, 32, 57, tzinfo=datetime.timezone.utc), - 'valid_to': datetime.datetime(2029, 5, 31, 11, 32, 57, tzinfo=datetime.timezone.utc), - 'subject': [('C', 'AU'), ('ST', 'Some-State'), ('O', 'pyas2lib'), ('CN', 'test')], - 'issuer': [('C', 'AU'), ('ST', 'Some-State'), ('O', 'pyas2lib'), ('CN', 'test')], - 'serial': 13747137503594840569 + "valid_from": datetime.datetime( + 2019, 6, 3, 11, 32, 57, tzinfo=datetime.timezone.utc + ), + "valid_to": datetime.datetime( + 2029, 5, 31, 11, 32, 57, tzinfo=datetime.timezone.utc + ), + "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 + "valid_from": None, + "valid_to": None, + "subject": None, + "issuer": None, + "serial": None, } # compare result of function with cert_info dict. - with open(os.path.join(TEST_DIR, 'cert_extract_private.cer'), 'rb') as fp: + with open(os.path.join(TEST_DIR, "cert_extract_private.cer"), "rb") as fp: assert utils.extract_certificate_info(fp.read()) == cert_info - with open(os.path.join(TEST_DIR, 'cert_extract_private.pem'), 'rb') as fp: + with open(os.path.join(TEST_DIR, "cert_extract_private.pem"), "rb") as fp: assert utils.extract_certificate_info(fp.read()) == cert_info - with open(os.path.join(TEST_DIR, 'cert_extract_public.cer'), 'rb') as fp: + with open(os.path.join(TEST_DIR, "cert_extract_public.cer"), "rb") as fp: assert utils.extract_certificate_info(fp.read()) == cert_info - with open(os.path.join(TEST_DIR, 'cert_extract_public.pem'), 'rb') as fp: + with open(os.path.join(TEST_DIR, "cert_extract_public.pem"), "rb") as fp: assert utils.extract_certificate_info(fp.read()) == cert_info - assert utils.extract_certificate_info(b'') == cert_empty + assert utils.extract_certificate_info(b"") == cert_empty diff --git a/pyas2lib/tests/test_with_mecas2.py b/pyas2lib/tests/test_with_mecas2.py index bcf58d8..19ab743 100644 --- a/pyas2lib/tests/test_with_mecas2.py +++ b/pyas2lib/tests/test_with_mecas2.py @@ -6,33 +6,30 @@ class TestMecAS2(Pyas2TestCase): - def setUp(self): self.org = as2.Organization( - as2_name='some_organization', + as2_name="some_organization", sign_key=self.private_key, - sign_key_pass='test', + sign_key_pass="test", decrypt_key=self.private_key, - decrypt_key_pass='test' + decrypt_key_pass="test", ) self.partner = as2.Partner( - as2_name='mecas2', + as2_name="mecas2", verify_cert=self.mecas2_public_key, encrypt_cert=self.mecas2_public_key, - validate_certs=False + validate_certs=False, ) def test_compressed_message(self): """ Test Compressed Message received from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join(TEST_DIR, 'mecas2_compressed.as2') - with open(received_file, 'rb') as fp: + received_file = os.path.join(TEST_DIR, "mecas2_compressed.as2") + with open(received_file, "rb") as fp: in_message = as2.Message() in_message.parse( - fp.read(), - find_org_cb=self.find_org, - find_partner_cb=self.find_partner + fp.read(), find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare the mic contents of the input and output messages @@ -43,98 +40,89 @@ def test_encrypted_message(self): """ Test Encrypted Message received from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join(TEST_DIR, 'mecas2_encrypted.as2') - with open(received_file, 'rb') as fp: + received_file = os.path.join(TEST_DIR, "mecas2_encrypted.as2") + with open(received_file, "rb") as fp: in_message = as2.Message() in_message.parse( - fp.read(), - find_org_cb=self.find_org, - find_partner_cb=self.find_partner + fp.read(), find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare the mic contents of the input and output messages self.assertTrue(in_message.encrypted) - self.assertEqual(in_message.enc_alg, 'tripledes_192_cbc') + self.assertEqual(in_message.enc_alg, "tripledes_192_cbc") self.assertEqual(self.test_data, in_message.content) def test_signed_message(self): """ Test Unencrypted Signed Uncompressed Message from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join(TEST_DIR, 'mecas2_signed.as2') - with open(received_file, 'rb') as fp: + received_file = os.path.join(TEST_DIR, "mecas2_signed.as2") + with open(received_file, "rb") as fp: in_message = as2.Message() in_message.parse( - fp.read(), - find_org_cb=self.find_org, - find_partner_cb=self.find_partner + fp.read(), find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare the mic contents of the input and output messages self.assertTrue(in_message.signed) - self.assertEqual(in_message.digest_alg, 'sha1') + self.assertEqual(in_message.digest_alg, "sha1") self.assertEqual(self.test_data, in_message.content) def test_encrypted_signed_message(self): """ Test Encrypted Signed Uncompressed Message from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join( - TEST_DIR, 'mecas2_signed_encrypted.as2') - with open(received_file, 'rb') as fp: + received_file = os.path.join(TEST_DIR, "mecas2_signed_encrypted.as2") + with open(received_file, "rb") as fp: in_message = as2.Message() in_message.parse( - fp.read(), - find_org_cb=self.find_org, - find_partner_cb=self.find_partner + fp.read(), find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare the mic contents of the input and output messages self.assertTrue(in_message.encrypted) - self.assertEqual(in_message.enc_alg, 'tripledes_192_cbc') + self.assertEqual(in_message.enc_alg, "tripledes_192_cbc") self.assertTrue(in_message.signed) - self.assertEqual(in_message.digest_alg, 'sha1') + self.assertEqual(in_message.digest_alg, "sha1") self.assertEqual(self.test_data, in_message.content) def test_encrypted_signed_compressed_message(self): """ Test Encrypted Signed Compressed Message from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join( - TEST_DIR, 'mecas2_compressed_signed_encrypted.as2') - with open(received_file, 'rb') as fp: + received_file = os.path.join(TEST_DIR, "mecas2_compressed_signed_encrypted.as2") + with open(received_file, "rb") as fp: in_message = as2.Message() in_message.parse( - fp.read(), - find_org_cb=self.find_org, - find_partner_cb=self.find_partner + fp.read(), find_org_cb=self.find_org, find_partner_cb=self.find_partner ) # Compare the mic contents of the input and output messages self.assertTrue(in_message.encrypted) - self.assertEqual(in_message.enc_alg, 'tripledes_192_cbc') + self.assertEqual(in_message.enc_alg, "tripledes_192_cbc") self.assertTrue(in_message.signed) - self.assertEqual(in_message.digest_alg, 'sha1') + self.assertEqual(in_message.digest_alg, "sha1") self.assertEqual(self.test_data, in_message.content) def test_unsigned_mdn(self): """ Test Unsigned MDN received from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join(TEST_DIR, 'mecas2_unsigned.mdn') - with open(received_file, 'rb') as fp: + received_file = os.path.join(TEST_DIR, "mecas2_unsigned.mdn") + with open(received_file, "rb") as fp: in_message = as2.Mdn() status, detailed_status = in_message.parse( - fp.read(), find_message_cb=self.find_message) + fp.read(), find_message_cb=self.find_message + ) - self.assertEqual(status, 'processed/error') - self.assertEqual(detailed_status, 'authentication-failed') + self.assertEqual(status, "processed/error") + self.assertEqual(detailed_status, "authentication-failed") def test_signed_mdn(self): """ Test Signed MDN received from Mendelson AS2""" # Parse the generated AS2 message as the partner - received_file = os.path.join(TEST_DIR, 'mecas2_signed.mdn') - with open(received_file, 'rb') as fp: + received_file = os.path.join(TEST_DIR, "mecas2_signed.mdn") + with open(received_file, "rb") as fp: in_message = as2.Mdn() in_message.parse(fp.read(), find_message_cb=self.find_message) @@ -148,5 +136,5 @@ def find_message(self, message_id, message_recipient): message = as2.Message() message.sender = self.org message.receiver = self.partner - message.mic = b'O4bvrm5t2YunRfwvZicNdEUmPaPZ9vUslX8loVLDck0=' + message.mic = b"O4bvrm5t2YunRfwvZicNdEUmPaPZ9vUslX8loVLDck0=" return message diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index 94846e6..f50f5c3 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -45,8 +45,12 @@ def _handle_text(self, msg): Handle writing the binary messages to prevent default behaviour of newline replacements. """ - if msg.get('Content-Transfer-Encoding') == 'binary' and \ - msg.get_content_subtype() in ['pkcs7-mime', 'pkcs7-signature']: + if msg.get( + "Content-Transfer-Encoding" + ) == "binary" and msg.get_content_subtype() in [ + "pkcs7-mime", + "pkcs7-signature", + ]: payload = msg.get_payload(decode=True) if payload is None: return @@ -80,13 +84,13 @@ def canonicalize(email_message: message.Message): :return: the standard representation of the email message in bytes """ - if email_message.get('Content-Transfer-Encoding') == 'binary': - message_header = '' + if email_message.get("Content-Transfer-Encoding") == "binary": + message_header = "" message_body = email_message.get_payload(decode=True) for k, v in email_message.items(): - message_header += '{}: {}\r\n'.format(k, v) - message_header += '\r\n' - return message_header.encode('utf-8') + message_body + message_header += "{}: {}\r\n".format(k, v) + message_header += "\r\n" + return message_header.encode("utf-8") + message_body else: return mime_to_bytes(email_message) @@ -98,19 +102,19 @@ def make_mime_boundary(text: str = None): """ width = len(repr(sys.maxsize - 1)) - fmt = '%%0%dd' % width + fmt = "%%0%dd" % width token = random.randrange(sys.maxsize) - boundary = ('=' * 15) + (fmt % token) + '==' + boundary = ("=" * 15) + (fmt % token) + "==" if text is None: return boundary b = boundary counter = 0 while True: - cre = re.compile('^--' + re.escape(b) + '(--)?$', re.MULTILINE) + cre = re.compile("^--" + re.escape(b) + "(--)?$", re.MULTILINE) if not cre.search(text): break - b = boundary + '.' + str(counter) + b = boundary + "." + str(counter) counter += 1 return b @@ -118,7 +122,7 @@ def make_mime_boundary(text: str = None): def extract_first_part(message: bytes, boundary: bytes): """Function to extract the first part of a multipart message.""" first_message = message.split(boundary)[1].lstrip() - if first_message.endswith(b'\r\n'): + if first_message.endswith(b"\r\n"): first_message = first_message[:-2] else: first_message = first_message[:-1] @@ -152,24 +156,24 @@ def split_pem(pem_bytes: bytes): :param pem_bytes: The pem data in bytes with multiple certs :return: yields a list of certificates contained in the pem file """ - started, pem_data = False, b'' + started, pem_data = False, b"" for line in pem_bytes.splitlines(False): - if line == b'' and not started: + if line == b"" and not started: continue - if line[0:5] in (b'-----', b'---- '): + if line[0:5] in (b"-----", b"---- "): if not started: started = True else: - pem_data = pem_data + line + b'\r\n' + pem_data = pem_data + line + b"\r\n" yield pem_data started = False - pem_data = b'' + pem_data = b"" if started: - pem_data = pem_data + line + b'\r\n' + pem_data = pem_data + line + b"\r\n" def verify_certificate_chain(cert_bytes, trusted_certs, ignore_self_signed=True): @@ -187,8 +191,7 @@ def verify_certificate_chain(cert_bytes, trusted_certs, ignore_self_signed=True) # Assuming the certificates are in PEM format in a trusted_certs list for _cert in trusted_certs: - store.add_cert( - crypto.load_certificate(crypto.FILETYPE_ASN1, _cert)) + store.add_cert(crypto.load_certificate(crypto.FILETYPE_ASN1, _cert)) # Create a certificate context using the store and the certificate store_ctx = crypto.X509StoreContext(store, certificate) @@ -200,7 +203,8 @@ def verify_certificate_chain(cert_bytes, trusted_certs, ignore_self_signed=True) except crypto.X509StoreContextError as e: raise AS2Exception( - 'Partner Certificate Invalid: %s' % e.args[-1][-1], 'invalid-certificate') + "Partner Certificate Invalid: %s" % e.args[-1][-1], "invalid-certificate" + ) def extract_certificate_info(cert: bytes): @@ -220,11 +224,11 @@ def extract_certificate_info(cert: bytes): # initialize the cert_info dictionary cert_info = { - 'valid_from': None, - 'valid_to': None, - 'subject': None, - 'issuer': None, - 'serial': None + "valid_from": None, + "valid_to": None, + "subject": None, + "issuer": None, + "serial": None, } # get certificate to DER list @@ -238,19 +242,21 @@ def extract_certificate_info(cert: bytes): 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").\ - replace(tzinfo=timezone.utc) - cert_info['valid_to'] = datetime.strptime( - certificate.get_notAfter().decode('utf8'), "%Y%m%d%H%M%SZ").\ - replace(tzinfo=timezone.utc) - 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() + cert_info["valid_from"] = datetime.strptime( + certificate.get_notBefore().decode("utf8"), "%Y%m%d%H%M%SZ" + ).replace(tzinfo=timezone.utc) + cert_info["valid_to"] = datetime.strptime( + certificate.get_notAfter().decode("utf8"), "%Y%m%d%H%M%SZ" + ).replace(tzinfo=timezone.utc) + 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 diff --git a/setup.cfg b/setup.cfg index 9af7e6f..6a53971 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,3 @@ [aliases] -test=pytest \ No newline at end of file +test=pytest + diff --git a/setup.py b/setup.py index ce30e78..d90344b 100644 --- a/setup.py +++ b/setup.py @@ -2,37 +2,34 @@ from setuptools import setup, find_packages install_requires = [ - 'asn1crypto==1.3.0', - 'oscrypto==1.2.0', - 'pyOpenSSL==19.1.0', + "asn1crypto==1.3.0", + "oscrypto==1.2.0", + "pyOpenSSL==19.1.0", ] if sys.version_info.minor == 6: - install_requires += [ - 'dataclasses==0.7' - ] + install_requires += ["dataclasses==0.7"] tests_require = [ - 'pytest==5.4.1', - 'pytest-cov==2.8.1', - 'coverage==5.0.4', + "pytest==5.4.1", + "pytest-cov==2.8.1", + "coverage==5.0.4", + "pylama==7.7.1", + "pytest-black==0.3.8", ] setup( - name='pyas2lib', + name="pyas2lib", description="Python library for building and parsing AS2 Messages", license="GNU GPL v2.0", url="https://github.com/abhishek-ram/pyas2-lib", long_description="Docs for this project are maintained at " - "https://github.com/abhishek-ram/pyas2-lib/blob/" - "master/README.md", - version='1.2.2', + "https://github.com/abhishek-ram/pyas2-lib/blob/" + "master/README.md", + version="1.2.2", author="Abhishek Ram", author_email="abhishek8816@gmail.com", - packages=find_packages( - where='.', - exclude=('test*',) - ), + packages=find_packages(where=".", exclude=("test*",)), classifiers=[ "Development Status :: 4 - Beta", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", @@ -44,9 +41,8 @@ "Programming Language :: Python :: 3.8", "Topic :: Security :: Cryptography", "Topic :: Communications", - ], - setup_requires=['pytest-runner'], + setup_requires=["pytest-runner"], install_requires=install_requires, tests_require=tests_require, ) From 15f6a7b5dafd4d49f9135b37833c4408b0eb49e7 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sat, 4 Apr 2020 19:55:50 +0530 Subject: [PATCH 45/66] add linter and fix linter raised issues --- .travis.yml | 5 ++--- pyas2lib/as2.py | 22 ++++++++++++++++++-- pyas2lib/cms.py | 53 ++++++++++++++++++++++--------------------------- setup.cfg | 20 +++++++++++++++++++ 4 files changed, 66 insertions(+), 34 deletions(-) diff --git a/.travis.yml b/.travis.yml index fc95967..eba774e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,9 @@ python: - '3.7' - '3.8' install: - - python setup.py install - - pip install pytest-cov + - pip install -e ".[testing]" script: - - pytest --cov=pyas2lib --cov-config .coveragerc + - pytest --cov=pyas2lib --cov-config .coveragerc --black --pylama after_success: - pip install codecov - codecov \ No newline at end of file diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 49d889a..a89f2ca 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -18,8 +18,26 @@ sign_message, verify_message, ) -from pyas2lib.constants import * -from pyas2lib.exceptions import * +from pyas2lib.constants import ( + AS2_VERSION, + ASYNCHRONOUS_MDN, + DIGEST_ALGORITHMS, + EDIINT_FEATURES, + ENCRYPTION_ALGORITHMS, + MDN_CONFIRM_TEXT, + MDN_FAILED_TEXT, + MDN_MODES, + SYNCHRONOUS_MDN, +) +from pyas2lib.exceptions import ( + AS2Exception, + DuplicateDocument, + ImproperlyConfigured, + InsufficientSecurityError, + IntegrityError, + MDNNotFound, + PartnerNotFound, +) from pyas2lib.utils import ( canonicalize, extract_first_part, diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 3a913e4..093c49a 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -6,16 +6,21 @@ from asn1crypto import cms, core, algos from oscrypto import asymmetric, symmetric, util -from pyas2lib.exceptions import * +from pyas2lib.exceptions import ( + AS2Exception, + DecompressionError, + DecryptionError, + IntegrityError, +) from pyas2lib.constants import DIGEST_ALGORITHMS def compress_message(data_to_compress): """Function compresses data and returns the generated ASN.1 - + :param data_to_compress: A byte string of the data to be compressed - - :return: A CMS ASN.1 byte string of the compressed data. + + :return: A CMS ASN.1 byte string of the compressed data. """ compressed_content = cms.ParsableOctetString(zlib.compress(data_to_compress)) return cms.ContentInfo( @@ -40,13 +45,13 @@ def compress_message(data_to_compress): def decompress_message(compressed_data): - """Function parses an ASN.1 compressed message and extracts/decompresses + """Function parses an ASN.1 compressed message and extracts/decompresses the original message. - - :param compressed_data: A CMS ASN.1 byte string containing the compressed + + :param compressed_data: A CMS ASN.1 byte string containing the compressed data. - :return: A byte string containing the decompressed original message. + :return: A byte string containing the decompressed original message. """ try: cms_content = cms.ContentInfo.load(compressed_data) @@ -63,16 +68,14 @@ def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): """Function encrypts data and returns the generated ASN.1 :param data_to_encrypt: A byte string of the data to be encrypted - :param enc_alg: The algorithm to be used for encrypting the data - :param encryption_cert: The certificate to be used for encrypting the data - :return: A CMS ASN.1 byte string of the encrypted data. + :return: A CMS ASN.1 byte string of the encrypted data. """ enc_alg_list = enc_alg.split("_") - cipher, key_length, mode = enc_alg_list[0], enc_alg_list[1], enc_alg_list[2] + cipher, key_length, _ = enc_alg_list[0], enc_alg_list[1], enc_alg_list[2] # Generate the symmetric encryption key and encrypt the message key = util.rand_bytes(int(key_length) // 8) @@ -175,15 +178,12 @@ def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): def decrypt_message(encrypted_data, decryption_key): - """Function parses an ASN.1 encrypted message and extracts/decrypts - the original message. + """Function parses an ASN.1 encrypted message and extracts/decrypts the original message. - :param encrypted_data: A CMS ASN.1 byte string containing the encrypted - data. - + :param encrypted_data: A CMS ASN.1 byte string containing the encrypted data. :param decryption_key: The key to be used for decrypting the data. - :return: A byte string containing the decrypted original message. + :return: A byte string containing the decrypted original message. """ cms_content = cms.ContentInfo.load(encrypted_data) @@ -250,17 +250,16 @@ def sign_message( :param data_to_sign: A byte string of the data to be signed. - :param digest_alg: - The digest algorithm to be used for generating the signature. + :param digest_alg: The digest algorithm to be used for generating the signature. :param sign_key: The key to be used for generating the signature. :param sign_alg: The algorithm to be used for signing the message. - :param use_signed_attributes: Optional attribute to indicate weather the + :param use_signed_attributes: Optional attribute to indicate weather the CMS signature attributes should be included in the signature or not. - :return: A CMS ASN.1 byte string of the signed data. + :return: A CMS ASN.1 byte string of the signed data. """ if use_signed_attributes: digest_func = hashlib.new(digest_alg) @@ -446,17 +445,13 @@ class SmimeCapabilities(core.Sequence): def verify_message(data_to_verify, signature, verify_cert): - """Function parses an ASN.1 encrypted message and extracts/decrypts - the original message. - - :param data_to_verify: - A byte string of the data to be verified against the signature. + """Function parses an ASN.1 encrypted message and extracts/decrypts the original message. + :param data_to_verify: A byte string of the data to be verified against the signature. :param signature: A CMS ASN.1 byte string containing the signature. - :param verify_cert: The certificate to be used for verifying the signature. - :return: The digest algorithm that was used in the signature. + :return: The digest algorithm that was used in the signature. """ cms_content = cms.ContentInfo.load(signature) diff --git a/setup.cfg b/setup.cfg index 6a53971..029bd22 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,23 @@ [aliases] test=pytest +[pylama:pycodestyle] +max_line_length = 100 + +[pylama:pylint] +max_line_length = 100 +ignore = E1101,R0902,R0903,W1203,C0103 + +[pylama:pydocstyle] +convention = numpy +ignore = D202 + +[pylama:pep8] +max_line_length = 100 + +[pylama] +format = pep8 +skip = venv/*,.tox/* +linters= pycodestyle,pydocstyle,pyflakes,pylint,pep8 +ignore = D203,D212,E231 + From 89f473692a74389a64f7f2b039419a6ccc77fbcd Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sat, 4 Apr 2020 20:00:10 +0530 Subject: [PATCH 46/66] add linter and fix linter raised issues --- .travis.yml | 2 +- setup.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eba774e..a9c61f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: - '3.7' - '3.8' install: - - pip install -e ".[testing]" + - pip install -e ".[tests]" script: - pytest --cov=pyas2lib --cov-config .coveragerc --black --pylama after_success: diff --git a/setup.py b/setup.py index d90344b..0bb52ec 100644 --- a/setup.py +++ b/setup.py @@ -45,4 +45,7 @@ setup_requires=["pytest-runner"], install_requires=install_requires, tests_require=tests_require, + extras_require={ + 'tests': tests_require, + }, ) From 2f0f9d8b4bb0a7f6f89a4f0f15b9085e3b0a79ba Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sat, 4 Apr 2020 20:01:56 +0530 Subject: [PATCH 47/66] add linter and fix linter raised issues --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 0bb52ec..992ded7 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,5 @@ setup_requires=["pytest-runner"], install_requires=install_requires, tests_require=tests_require, - extras_require={ - 'tests': tests_require, - }, + extras_require={"tests": tests_require,}, ) From 6885125d328d3ca19bbdb1a9534c9835292624ea Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 5 Apr 2020 10:10:13 +0530 Subject: [PATCH 48/66] Bump version and update the changelog --- CHANGELOG.md | 6 ++++++ pyas2lib/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ce13a9..f542b1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 1.3.0 - 2020-04-05 +* Fix and update the SMIME capabilities in the Signed attributes of a signature +* Update the versions of crypto dependencies and related changes +* Use black and pylama as code formatter and linter +* Increase test coverage and add support for python 3.8 + ## 1.2.2 - 2019-06-26 * Handle MDNNotfound correctly when parsing an mdn diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index 7253cbe..aeaf064 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -9,7 +9,7 @@ from pyas2lib.as2 import Organization from pyas2lib.as2 import Partner -__version__ = "1.2.2" +__version__ = "1.3.0" __all__ = [ diff --git a/setup.py b/setup.py index 992ded7..6accc71 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version="1.2.2", + version="1.3.0", author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages(where=".", exclude=("test*",)), From 241ca9db78af65f554941c9e73798886c32c14b9 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 12 Apr 2020 19:27:46 +0530 Subject: [PATCH 49/66] set the requirement for dataclasses correctly --- CHANGELOG.md | 3 +++ pyas2lib/__init__.py | 2 +- setup.py | 7 ++----- tox.ini | 1 - 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f542b1d..c7384ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Release History +## 1.3.1 - 2020-04-12 +* Use correct format for setting dataclasses requirement for python 3.6 + ## 1.3.0 - 2020-04-05 * Fix and update the SMIME capabilities in the Signed attributes of a signature * Update the versions of crypto dependencies and related changes diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index aeaf064..ed525f2 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -9,7 +9,7 @@ from pyas2lib.as2 import Organization from pyas2lib.as2 import Partner -__version__ = "1.3.0" +__version__ = "1.3.1" __all__ = [ diff --git a/setup.py b/setup.py index 6accc71..5a79a02 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,12 @@ -import sys from setuptools import setup, find_packages install_requires = [ "asn1crypto==1.3.0", "oscrypto==1.2.0", "pyOpenSSL==19.1.0", + "dataclasses==0.7;python_version=='3.6'", ] -if sys.version_info.minor == 6: - install_requires += ["dataclasses==0.7"] - tests_require = [ "pytest==5.4.1", "pytest-cov==2.8.1", @@ -26,7 +23,7 @@ long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version="1.3.0", + version="1.3.1", author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages(where=".", exclude=("test*",)), diff --git a/tox.ini b/tox.ini index 3e6822f..49f0c46 100644 --- a/tox.ini +++ b/tox.ini @@ -9,4 +9,3 @@ envlist = py36, py37, py38 [testenv] commands = {envpython} setup.py test deps = - From 56126aaeefa6cdd4dfd2213b224a38620709e83a Mon Sep 17 00:00:00 2001 From: Wassilios Lytras Date: Fri, 8 May 2020 15:39:25 +0200 Subject: [PATCH 50/66] Determine correct signature algorithm. --- pyas2lib/cms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 093c49a..e67492e 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -464,7 +464,7 @@ def verify_message(data_to_verify, signature, verify_cert): if digest_alg not in DIGEST_ALGORITHMS: raise Exception("Unsupported Digest Algorithm") - sig_alg = signer["signature_algorithm"]["algorithm"].native + sig_alg = signer["signature_algorithm"].signature_algo sig = signer["signature"].native signed_data = data_to_verify From 1beb1ed468ccb828ccf24bcc19a549a40eebaf10 Mon Sep 17 00:00:00 2001 From: Fabian Brosig Date: Mon, 11 May 2020 17:01:47 +0200 Subject: [PATCH 51/66] Dont replace "\n" newlines in application/octet-stream payload with "\r\n" When building an AS/2 message with data of content-type "application/octet-stream", the payload's "\n" newlines got replaced with "\r\n", leading to corrupted message payloads. --- pyas2lib/tests/test_advanced.py | 3 +-- pyas2lib/utils.py | 10 ++++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pyas2lib/tests/test_advanced.py b/pyas2lib/tests/test_advanced.py index 5179513..10f83d1 100644 --- a/pyas2lib/tests/test_advanced.py +++ b/pyas2lib/tests/test_advanced.py @@ -52,9 +52,8 @@ def test_binary_message(self): ) # Compare the mic contents of the input and output messages - # self.assertEqual(original_message, - # in_message.payload.get_payload(decode=True)) self.assertEqual(status, "processed") + self.assertEqual(original_message, in_message.payload.get_payload(decode=True)) self.assertTrue(in_message.signed) self.assertTrue(in_message.encrypted) self.assertEqual(out_message.mic, in_message.mic) diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index f50f5c3..200294c 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -45,12 +45,10 @@ def _handle_text(self, msg): Handle writing the binary messages to prevent default behaviour of newline replacements. """ - if msg.get( - "Content-Transfer-Encoding" - ) == "binary" and msg.get_content_subtype() in [ - "pkcs7-mime", - "pkcs7-signature", - ]: + if msg.get_content_type() == "application/octet-stream" or ( + msg.get("Content-Transfer-Encoding") == "binary" + and msg.get_content_subtype() in ["pkcs7-mime", "pkcs7-signature",] + ): payload = msg.get_payload(decode=True) if payload is None: return From b1540c2fa01ae524d9e5d615ca82f917e72fdc31 Mon Sep 17 00:00:00 2001 From: Wassilios Lytras Date: Tue, 12 May 2020 09:04:50 +0200 Subject: [PATCH 52/66] Raise exception when digest_alg is not known. Convert digest_alg to lower case to prevent TypeError in asymmetric. --- pyas2lib/cms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index e67492e..1395aa1 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -261,6 +261,10 @@ def sign_message( :return: A CMS ASN.1 byte string of the signed data. """ + digest_alg = digest_alg.lower() + if digest_alg not in DIGEST_ALGORITHMS: + raise Exception('Unsupported Digest Algorithm') + if use_signed_attributes: digest_func = hashlib.new(digest_alg) digest_func.update(data_to_sign) From 1fc6b28eb27d25488e1db98ec9eb9e4550538749 Mon Sep 17 00:00:00 2001 From: Wassilios Lytras Date: Tue, 12 May 2020 14:05:10 +0200 Subject: [PATCH 53/66] Formatting --- pyas2lib/cms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 1395aa1..69a0bfe 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -263,7 +263,7 @@ def sign_message( """ digest_alg = digest_alg.lower() if digest_alg not in DIGEST_ALGORITHMS: - raise Exception('Unsupported Digest Algorithm') + raise Exception("Unsupported Digest Algorithm") if use_signed_attributes: digest_func = hashlib.new(digest_alg) From 84715210231846716d9172f1e8a47fa8be8e33a7 Mon Sep 17 00:00:00 2001 From: Wassilios Lytras Date: Tue, 12 May 2020 14:18:30 +0200 Subject: [PATCH 54/66] Adding additional test --- pyas2lib/cms.py | 2 +- pyas2lib/tests/test_cms.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 69a0bfe..9ffbc60 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -263,7 +263,7 @@ def sign_message( """ digest_alg = digest_alg.lower() if digest_alg not in DIGEST_ALGORITHMS: - raise Exception("Unsupported Digest Algorithm") + raise AS2Exception("Unsupported Digest Algorithm") if use_signed_attributes: digest_func = hashlib.new(digest_alg) diff --git a/pyas2lib/tests/test_cms.py b/pyas2lib/tests/test_cms.py index 9494bfd..7a898e8 100644 --- a/pyas2lib/tests/test_cms.py +++ b/pyas2lib/tests/test_cms.py @@ -58,6 +58,15 @@ def test_signing(): b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pssa" ) + # Test unsupported digest alg + with pytest.raises(AS2Exception): + cms.sign_message( + b"data", + digest_alg="sha-256", + sign_key=sign_key, + use_signed_attributes=False, + ) + def test_encryption(): """Test the encryption and decryption functions.""" From 13bda602f72585ebdb9950dc2ca87d388d8eb613 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sat, 15 Aug 2020 12:56:19 +0100 Subject: [PATCH 55/66] Initial fix for binary messages. Missing tests for Message.build --- pyas2lib/as2.py | 6 +++- pyas2lib/tests/test_utils.py | 67 ++++++++++++++++++++++++++++++++++-- pyas2lib/utils.py | 8 ++--- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index a89f2ca..877c553 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -381,7 +381,11 @@ def build( self.payload = email_message.Message() self.payload.set_payload(data) self.payload.set_type(content_type) - encoders.encode_7or8bit(self.payload) + + if content_type.lower().startswith('application/octet-stream'): + self.payload['Content-Transfer-Encoding'] = 'binary' + else: + encoders.encode_7or8bit(self.payload) if filename: self.payload.add_header( diff --git a/pyas2lib/tests/test_utils.py b/pyas2lib/tests/test_utils.py index 23b31d2..10411e0 100644 --- a/pyas2lib/tests/test_utils.py +++ b/pyas2lib/tests/test_utils.py @@ -15,8 +15,10 @@ def test_quoting(): assert utils.quote_as2name("PYAS2 LIB") == '"PYAS2 LIB"' -def test_bytes_generator(): - """Test the email bytes generator class.""" +def test_mime_to_bytes_empty_message(): + """ + It will generate the headers with an empty body + """ message = Message() message.set_type("application/pkcs7-mime") assert ( @@ -25,6 +27,67 @@ def test_bytes_generator(): ) +def test_mime_to_bytes_unix_text(): + """ + For non-binary content types, + it converts unix new lines to network newlines. + """ + message = Message() + message.set_type("application/xml") + message.set_payload('Some line.\nAnother line.') + + result = utils.mime_to_bytes(message) + + assert ( + b"MIME-Version: 1.0\r\n" + b"Content-Type: application/xml\r\n" + b"\r\n" + b"Some line.\r\n" + b"Another line." + ) == result + + +def test_mime_to_bytes_octet_stream(): + """ + For binary octet-stream content types, + it does not converts unix new lines to network newlines. + """ + message = Message() + message.set_type("application/octet-stream") + message.set_payload('Some line.\nAnother line.\n') + + result = utils.mime_to_bytes(message) + + assert ( + b"MIME-Version: 1.0\r\n" + b"Content-Type: application/octet-stream\r\n" + b"\r\n" + b"Some line.\n" + b"Another line.\n" + ) == result + + +def test_mime_to_bytes_binary(): + """ + It makes no conversion for binary content encoding. + """ + message = Message() + message.set_type("any/type") + message['Content-Transfer-Encoding'] = 'binary' + message.set_payload('Some line.\nAnother line.\n') + + result = utils.mime_to_bytes(message) + + assert ( + b"MIME-Version: 1.0\r\n" + b"Content-Type: any/type\r\n" + b"Content-Transfer-Encoding: binary\r\n" + b"\r\n" + b"Some line.\n" + b"Another line.\n" + ) == result + + def test_make_boundary(): """Test the function for creating a boundary for multipart messages.""" assert utils.make_mime_boundary(text="123456") is not None diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index 200294c..cec1c58 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -45,10 +45,10 @@ def _handle_text(self, msg): Handle writing the binary messages to prevent default behaviour of newline replacements. """ - if msg.get_content_type() == "application/octet-stream" or ( - msg.get("Content-Transfer-Encoding") == "binary" - and msg.get_content_subtype() in ["pkcs7-mime", "pkcs7-signature",] - ): + if ( + msg.get_content_type() == "application/octet-stream" + or msg.get("Content-Transfer-Encoding") == "binary" + ): payload = msg.get_payload(decode=True) if payload is None: return From 24b01bbcd8d01b9cca663f8366dcdb9d369b3952 Mon Sep 17 00:00:00 2001 From: Wassilios Lytras Date: Wed, 21 Oct 2020 12:55:58 +0200 Subject: [PATCH 56/66] If no Original-Recipient provided, fall back to mandatory Final-Recipient. --- pyas2lib/as2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index a89f2ca..67ee322 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -978,5 +978,8 @@ def detect_mdn(self): if part.get_content_type() == "message/disposition-notification": mdn = part.get_payload()[0] message_id = mdn.get("Original-Message-ID").strip("<>") - message_recipient = mdn.get("Original-Recipient").split(";")[1].strip() + if mdn.get("Original-Recipient"): + message_recipient = mdn.get("Original-Recipient").split(";")[1].strip() + else: + message_recipient = mdn.get("Final-Recipient").split(";")[1].strip() return message_id, message_recipient From 1f62d26ad71bdfdac25a20ecd6500b0f9edbe6cc Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 1 Nov 2020 16:43:56 +0530 Subject: [PATCH 57/66] disable certificate validation for SI test cases --- pyas2lib/tests/test_advanced.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyas2lib/tests/test_advanced.py b/pyas2lib/tests/test_advanced.py index 10f83d1..4e9650d 100644 --- a/pyas2lib/tests/test_advanced.py +++ b/pyas2lib/tests/test_advanced.py @@ -473,6 +473,7 @@ def setUp(self): verify_cert_ca=self.sb2bi_public_ca, encrypt_cert=self.sb2bi_public_key, encrypt_cert_ca=self.sb2bi_public_ca, + validate_certs=False ) self.partner.load_verify_cert() self.partner.load_encrypt_cert() From 8e811cb17ad4cab160a10e51d6e117f06f3a8473 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 1 Nov 2020 16:47:40 +0530 Subject: [PATCH 58/66] disable certificate validation for SI test cases --- pyas2lib/as2.py | 9 +++++---- pyas2lib/tests/test_advanced.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 67ee322..ff84f56 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -978,8 +978,9 @@ def detect_mdn(self): if part.get_content_type() == "message/disposition-notification": mdn = part.get_payload()[0] message_id = mdn.get("Original-Message-ID").strip("<>") - if mdn.get("Original-Recipient"): - message_recipient = mdn.get("Original-Recipient").split(";")[1].strip() - else: - message_recipient = mdn.get("Final-Recipient").split(";")[1].strip() + message_recipient = None + if "Original-Recipient" in mdn: + message_recipient = mdn["Original-Recipient"].split(";")[1].strip() + elif "Final-Recipient" in mdn: + message_recipient = mdn["Final-Recipient"].split(";")[1].strip() return message_id, message_recipient diff --git a/pyas2lib/tests/test_advanced.py b/pyas2lib/tests/test_advanced.py index 4e9650d..5947194 100644 --- a/pyas2lib/tests/test_advanced.py +++ b/pyas2lib/tests/test_advanced.py @@ -473,7 +473,7 @@ def setUp(self): verify_cert_ca=self.sb2bi_public_ca, encrypt_cert=self.sb2bi_public_key, encrypt_cert_ca=self.sb2bi_public_ca, - validate_certs=False + validate_certs=False, ) self.partner.load_verify_cert() self.partner.load_encrypt_cert() From 9f5d27cd4b1e5d86c3fe328fcfd1347f93857543 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 1 Nov 2020 16:53:08 +0530 Subject: [PATCH 59/66] run black to fix linting --- pyas2lib/as2.py | 4 ++-- pyas2lib/tests/test_utils.py | 8 ++++---- pyas2lib/utils.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 6b0aec9..4a42abc 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -382,8 +382,8 @@ def build( self.payload.set_payload(data) self.payload.set_type(content_type) - if content_type.lower().startswith('application/octet-stream'): - self.payload['Content-Transfer-Encoding'] = 'binary' + if content_type.lower().startswith("application/octet-stream"): + self.payload["Content-Transfer-Encoding"] = "binary" else: encoders.encode_7or8bit(self.payload) diff --git a/pyas2lib/tests/test_utils.py b/pyas2lib/tests/test_utils.py index 10411e0..72a9e7a 100644 --- a/pyas2lib/tests/test_utils.py +++ b/pyas2lib/tests/test_utils.py @@ -34,7 +34,7 @@ def test_mime_to_bytes_unix_text(): """ message = Message() message.set_type("application/xml") - message.set_payload('Some line.\nAnother line.') + message.set_payload("Some line.\nAnother line.") result = utils.mime_to_bytes(message) @@ -54,7 +54,7 @@ def test_mime_to_bytes_octet_stream(): """ message = Message() message.set_type("application/octet-stream") - message.set_payload('Some line.\nAnother line.\n') + message.set_payload("Some line.\nAnother line.\n") result = utils.mime_to_bytes(message) @@ -73,8 +73,8 @@ def test_mime_to_bytes_binary(): """ message = Message() message.set_type("any/type") - message['Content-Transfer-Encoding'] = 'binary' - message.set_payload('Some line.\nAnother line.\n') + message["Content-Transfer-Encoding"] = "binary" + message.set_payload("Some line.\nAnother line.\n") result = utils.mime_to_bytes(message) diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index cec1c58..dd2bbe6 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -48,7 +48,7 @@ def _handle_text(self, msg): if ( msg.get_content_type() == "application/octet-stream" or msg.get("Content-Transfer-Encoding") == "binary" - ): + ): payload = msg.get_payload(decode=True) if payload is None: return From 29a84d821dddb9c6d2694dd055968c50ed43ca21 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 1 Nov 2020 17:56:30 +0530 Subject: [PATCH 60/66] Use pylava as the linter and freeze versions of test libs --- .travis.yml | 2 +- pyas2lib/as2.py | 85 ++++++++++++++++++------------------ pyas2lib/cms.py | 17 +++++--- pyas2lib/exceptions.py | 2 +- pyas2lib/tests/test_cms.py | 4 +- pyas2lib/tests/test_utils.py | 4 +- pyas2lib/utils.py | 34 +++++++-------- setup.cfg | 17 ++++---- setup.py | 12 +++-- 9 files changed, 93 insertions(+), 84 deletions(-) diff --git a/.travis.yml b/.travis.yml index a9c61f4..63dcf98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: install: - pip install -e ".[tests]" script: - - pytest --cov=pyas2lib --cov-config .coveragerc --black --pylama + - pytest --cov=pyas2lib --cov-config .coveragerc --black --pylava after_success: - pip install codecov - codecov \ No newline at end of file diff --git a/pyas2lib/as2.py b/pyas2lib/as2.py index 4a42abc..7eaba35 100644 --- a/pyas2lib/as2.py +++ b/pyas2lib/as2.py @@ -1,3 +1,4 @@ +"""Define the core functions/classes of the pyas2 package.""" import logging import hashlib import binascii @@ -54,7 +55,7 @@ @dataclass -class Organization(object): +class Organization: """ Class represents an AS2 organization and defines the certificates and settings to be used when sending and receiving messages. @@ -124,7 +125,7 @@ def load_key(key_str: bytes, key_pass: str): @dataclass -class Partner(object): +class Partner: """ Class represents an AS2 partner and defines the certificates and settings to be used when sending and receiving messages. @@ -217,6 +218,7 @@ def __post_init__(self): ) def load_verify_cert(self): + """Load the verification certificate of the partner and returned the parsed cert.""" if self.validate_certs: # Convert the certificate to DER format cert = pem_to_der(self.verify_cert, return_multiple=False) @@ -235,6 +237,7 @@ def load_verify_cert(self): return asymmetric.load_certificate(self.verify_cert) def load_encrypt_cert(self): + """Load the encryption certificate of the partner and returned the parsed cert.""" if self.validate_certs: # Convert the certificate to DER format cert = pem_to_der(self.encrypt_cert, return_multiple=False) @@ -253,7 +256,7 @@ def load_encrypt_cert(self): return asymmetric.load_certificate(self.encrypt_cert) -class Message(object): +class Message: """Class for handling AS2 messages. Includes functions for both parsing and building messages. """ @@ -291,19 +294,20 @@ def content(self): temp = message_bytes.split(boundary) temp.pop(0) return boundary + boundary.join(temp) - else: - content = self.payload.get_payload(decode=True) - return content + + content = self.payload.get_payload(decode=True) + return content @property def headers(self): + """Return the headers in the payload as a dictionary.""" if self.payload: return dict(self.payload.items()) - else: - return {} + return {} @property def headers_str(self): + """Return the headers in the payload as a string.""" message_header = "" if self.payload: for k, v in self.headers.items(): @@ -346,10 +350,10 @@ def build( """ # Validations - assert type(data) is bytes, "Parameter data must be of bytes type." + assert isinstance(data, bytes), "Parameter data must be of bytes type." additional_headers = additional_headers if additional_headers else {} - assert type(additional_headers) is dict + assert isinstance(additional_headers, dict) if self.receiver.sign and not self.sender.sign_key: raise ImproperlyConfigured( @@ -641,43 +645,42 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None) if not self.compressed: self.compressed, self.payload = self._decompress_data(self.payload) - except Exception as e: + except Exception as e: # pylint: disable=W0703 status = getattr(e, "disposition_type", "processed/Error") detailed_status = getattr( e, "disposition_modifier", "unexpected-processing-error" ) exception = (e, traceback.format_exc()) logger.error(f"Failed to parse AS2 message\n: {traceback.format_exc()}") - finally: - # Update the payload headers with the original headers - for k, v in as2_headers.items(): - if self.payload.get(k) and k.lower() != "content-disposition": - del self.payload[k] - self.payload.add_header(k, v) + # Update the payload headers with the original headers + for k, v in as2_headers.items(): + if self.payload.get(k) and k.lower() != "content-disposition": + del self.payload[k] + self.payload.add_header(k, v) - if as2_headers.get("disposition-notification-to"): - mdn_mode = SYNCHRONOUS_MDN + if as2_headers.get("disposition-notification-to"): + mdn_mode = SYNCHRONOUS_MDN - mdn_url = as2_headers.get("receipt-delivery-option") - if mdn_url: - mdn_mode = ASYNCHRONOUS_MDN + mdn_url = as2_headers.get("receipt-delivery-option") + if mdn_url: + mdn_mode = ASYNCHRONOUS_MDN - digest_alg = as2_headers.get("disposition-notification-options") - if digest_alg: - digest_alg = digest_alg.split(";")[-1].split(",")[-1].strip() + digest_alg = as2_headers.get("disposition-notification-options") + if digest_alg: + digest_alg = digest_alg.split(";")[-1].split(",")[-1].strip() - logger.debug( - f"Building the MDN for message {self.message_id} with status {status} " - f"and detailed-status {detailed_status}." - ) - mdn = Mdn(mdn_mode=mdn_mode, mdn_url=mdn_url, digest_alg=digest_alg) - mdn.build(message=self, status=status, detailed_status=detailed_status) + logger.debug( + f"Building the MDN for message {self.message_id} with status {status} " + f"and detailed-status {detailed_status}." + ) + mdn = Mdn(mdn_mode=mdn_mode, mdn_url=mdn_url, digest_alg=digest_alg) + mdn.build(message=self, status=status, detailed_status=detailed_status) - return status, exception, mdn + return status, exception, mdn -class Mdn(object): +class Mdn: """Class for handling AS2 MDNs. Includes functions for both parsing and building them. """ @@ -700,18 +703,18 @@ def content(self): temp = message_bytes.split(boundary) temp.pop(0) return boundary + boundary.join(temp) - else: - return "" + return "" @property def headers(self): + """Return the headers in the payload as a dictionary.""" if self.payload: return dict(self.payload.items()) - else: - return {} + return {} @property def headers_str(self): + """Return the headers in the payload as a string.""" message_header = "" if self.payload: for k, v in self.headers.items(): @@ -947,16 +950,14 @@ def parse(self, raw_content, find_message_cb): except MDNNotFound: status = "failed/Failure" detailed_status = "mdn-not-found" - except Exception as e: + except Exception as e: # pylint: disable=W0703 status = "failed/Failure" detailed_status = f"Failed to parse received MDN. {e}" logger.error(f"Failed to parse AS2 MDN\n: {traceback.format_exc()}") - - finally: - return status, detailed_status + return status, detailed_status def detect_mdn(self): - """ Function checks if the received raw message is an AS2 MDN or not. + """Function checks if the received raw message is an AS2 MDN or not. :raises MDNNotFound: If the received payload is not an MDN then this exception is raised. diff --git a/pyas2lib/cms.py b/pyas2lib/cms.py index 9ffbc60..02fe8c7 100644 --- a/pyas2lib/cms.py +++ b/pyas2lib/cms.py @@ -1,3 +1,4 @@ +"""Define functions related to the CMS operations such as encrypting, signature, etc.""" import hashlib import zlib from collections import OrderedDict @@ -57,8 +58,7 @@ def decompress_message(compressed_data): cms_content = cms.ContentInfo.load(compressed_data) if cms_content["content_type"].native == "compressed_data": return cms_content["content"].decompressed - else: - raise DecompressionError("Compressed data not found in ASN.1 ") + raise DecompressionError("Compressed data not found in ASN.1 ") except Exception as e: raise DecompressionError("Decompression failed with cause: {}".format(e)) @@ -103,7 +103,11 @@ def encrypt_message(data_to_encrypt, enc_alg, encryption_cert): elif cipher == "rc4": algorithm_id = "1.2.840.113549.3.4" encrypted_content = symmetric.rc4_encrypt(key, data_to_encrypt) - enc_alg_asn1 = algos.EncryptionAlgorithm({"algorithm": algorithm_id,}) + enc_alg_asn1 = algos.EncryptionAlgorithm( + { + "algorithm": algorithm_id, + } + ) elif cipher == "aes": if key_length == "128": @@ -271,6 +275,8 @@ def sign_message( message_digest = digest_func.digest() class SmimeCapability(core.Sequence): + """"Define the possible list of Smime Capability.""" + _fields = [ ("0", core.Any, {"optional": True}), ("1", core.Any, {"optional": True}), @@ -280,6 +286,8 @@ class SmimeCapability(core.Sequence): ] class SmimeCapabilities(core.Sequence): + """"Define the Smime Capabilities supported by pyas2.""" + _fields = [ ("0", SmimeCapability), ("1", SmimeCapability, {"optional": True}), @@ -504,9 +512,6 @@ def verify_message(data_to_verify, signature, verify_cert): else: raise AS2Exception("Unsupported Signature Algorithm") except Exception as e: - import traceback - - traceback.print_exc() raise IntegrityError("Failed to verify message signature: {}".format(e)) else: raise IntegrityError("Signed data not found in ASN.1 ") diff --git a/pyas2lib/exceptions.py b/pyas2lib/exceptions.py index 3edccb8..1b1c2d6 100644 --- a/pyas2lib/exceptions.py +++ b/pyas2lib/exceptions.py @@ -82,7 +82,7 @@ class IntegrityError(AS2Exception): class UnexpectedError(AS2Exception): """A catch all exception to be raised for any error found while parsing - a received AS2 message""" + a received AS2 message""" disposition_type = "processed/Error" disposition_modifier = "unexpected-processing-error" diff --git a/pyas2lib/tests/test_cms.py b/pyas2lib/tests/test_cms.py index 7a898e8..34655db 100644 --- a/pyas2lib/tests/test_cms.py +++ b/pyas2lib/tests/test_cms.py @@ -16,7 +16,9 @@ INVALID_DATA = cms.cms.ContentInfo( - {"content_type": cms.cms.ContentType("data"),} + { + "content_type": cms.cms.ContentType("data"), + } ).dump() diff --git a/pyas2lib/tests/test_utils.py b/pyas2lib/tests/test_utils.py index 72a9e7a..f1607e5 100644 --- a/pyas2lib/tests/test_utils.py +++ b/pyas2lib/tests/test_utils.py @@ -114,8 +114,8 @@ def test_cert_verification(): def test_extract_certificate_info(): - """ Test case that extracts data from private and public certificates - in PEM or DER format""" + """Test case that extracts data from private and public certificates + in PEM or DER format""" cert_info = { "valid_from": datetime.datetime( diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index dd2bbe6..a76ade9 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -1,16 +1,19 @@ +"""Define utility functions used by the pyas2-lib package.""" + import email import random import re import sys -from OpenSSL import crypto -from asn1crypto import pem -from email import policy +from datetime import datetime, timezone from email import message +from email import policy from email.generator import BytesGenerator from io import BytesIO +from OpenSSL import crypto +from asn1crypto import pem + from pyas2lib.exceptions import AS2Exception -from datetime import datetime, timezone def unquote_as2name(quoted_name: str): @@ -33,8 +36,7 @@ def quote_as2name(unquoted_name: str): if re.search(r'[\\" ]', unquoted_name, re.M): return '"' + email.utils.quote(unquoted_name) + '"' - else: - return unquoted_name + return unquoted_name class BinaryBytesGenerator(BytesGenerator): @@ -52,8 +54,7 @@ def _handle_text(self, msg): payload = msg.get_payload(decode=True) if payload is None: return - else: - self._fp.write(payload) + self._fp.write(payload) else: super()._handle_text(msg) @@ -89,8 +90,8 @@ def canonicalize(email_message: message.Message): message_header += "{}: {}\r\n".format(k, v) message_header += "\r\n" return message_header.encode("utf-8") + message_body - else: - return mime_to_bytes(email_message) + + return mime_to_bytes(email_message) def make_mime_boundary(text: str = None): @@ -117,9 +118,9 @@ def make_mime_boundary(text: str = None): return b -def extract_first_part(message: bytes, boundary: bytes): - """Function to extract the first part of a multipart message.""" - first_message = message.split(boundary)[1].lstrip() +def extract_first_part(message_content: bytes, boundary: bytes): + """Extract the first part of a multipart message.""" + first_message = message_content.split(boundary)[1].lstrip() if first_message.endswith(b"\r\n"): first_message = first_message[:-2] else: @@ -128,8 +129,7 @@ def extract_first_part(message: bytes, boundary: bytes): def pem_to_der(cert: bytes, return_multiple: bool = True): - """Converts a given certificate or list to PEM format.""" - + """Convert a given certificate or list to PEM format.""" # initialize the certificate array cert_list = [] @@ -143,8 +143,7 @@ def pem_to_der(cert: bytes, return_multiple: bool = True): # return multiple if return_multiple is set else first element if return_multiple: return cert_list - else: - return cert_list.pop() + return cert_list.pop() def split_pem(pem_bytes: bytes): @@ -219,7 +218,6 @@ def extract_certificate_info(cert: bytes): issuer (list of name, value tuples) serial (int) """ - # initialize the cert_info dictionary cert_info = { "valid_from": None, diff --git a/setup.cfg b/setup.cfg index 029bd22..927df06 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,23 +1,22 @@ [aliases] test=pytest -[pylama:pycodestyle] +[pylava:pycodestyle] max_line_length = 100 -[pylama:pylint] +[pylava:pylint] max_line_length = 100 ignore = E1101,R0902,R0903,W1203,C0103 -[pylama:pydocstyle] +[pylava:pydocstyle] convention = numpy ignore = D202 -[pylama:pep8] +[pylava:pep8] max_line_length = 100 -[pylama] +[pylava] format = pep8 -skip = venv/*,.tox/* -linters= pycodestyle,pydocstyle,pyflakes,pylint,pep8 -ignore = D203,D212,E231 - +skip = venv/*,.tox/*,*/tests/*,setup.py +linters= pycodestyle,pyflakes,pylint,pep8 +ignore = D203,D212,E231,C0330,R0912,R0914,W1202,R1702 diff --git a/setup.py b/setup.py index 5a79a02..0670b7c 100644 --- a/setup.py +++ b/setup.py @@ -8,11 +8,13 @@ ] tests_require = [ - "pytest==5.4.1", + "pytest==6.1.2", "pytest-cov==2.8.1", "coverage==5.0.4", - "pylama==7.7.1", - "pytest-black==0.3.8", + "pylava-pylint==0.0.3", + "pylint==2.4.4", + "black==20.8b1", + "pytest-black==0.3.12", ] setup( @@ -42,5 +44,7 @@ setup_requires=["pytest-runner"], install_requires=install_requires, tests_require=tests_require, - extras_require={"tests": tests_require,}, + extras_require={ + "tests": tests_require, + }, ) From 06b5eabb04c56a0295b30f5a0f444d4f590ae036 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 1 Nov 2020 20:07:16 +0530 Subject: [PATCH 61/66] Remove support for python 3.6 --- .travis.yml | 1 - setup.cfg | 2 +- setup.py | 3 +-- tox.ini | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 63dcf98..2170dff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ dist: xenial language: python python: - - '3.6' - '3.7' - '3.8' install: diff --git a/setup.cfg b/setup.cfg index 927df06..aa5e6ec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,4 +19,4 @@ max_line_length = 100 format = pep8 skip = venv/*,.tox/*,*/tests/*,setup.py linters= pycodestyle,pyflakes,pylint,pep8 -ignore = D203,D212,E231,C0330,R0912,R0914,W1202,R1702 +ignore = D203,D212,E231,C0330,R0912,R0914,W1202,R1702,C0114 diff --git a/setup.py b/setup.py index 0670b7c..f178f5e 100644 --- a/setup.py +++ b/setup.py @@ -11,8 +11,8 @@ "pytest==6.1.2", "pytest-cov==2.8.1", "coverage==5.0.4", - "pylava-pylint==0.0.3", "pylint==2.4.4", + "pylava-pylint==0.0.3", "black==20.8b1", "pytest-black==0.3.12", ] @@ -35,7 +35,6 @@ "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Security :: Cryptography", diff --git a/tox.ini b/tox.ini index 49f0c46..f209c38 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py36, py37, py38 +envlist = py37, py38 [testenv] commands = {envpython} setup.py test From 1069918556d7bead9effdf9063b2e409f47fc926 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 1 Nov 2020 20:21:04 +0530 Subject: [PATCH 62/66] Bump the version of pyas2 to 1.3.2 --- CHANGELOG.md | 8 ++++++++ pyas2lib/__init__.py | 2 +- setup.py | 3 +-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7384ff..55c6128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Release History +## 1.3.2 - 2020-11-01 +* Use `signature_algo` attribute when detecting the signature algorithm +* Raise exception when unknown `digest_alg` is passed to the sign function +* Add proper support for handling binary messages +* Look for `Final-Recipient` if `Original-Recipient` is not present in the MDN +* Remove support for python 3.6 +* Fix linting and change the linter to pylava + ## 1.3.1 - 2020-04-12 * Use correct format for setting dataclasses requirement for python 3.6 diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index ed525f2..fe44dd4 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -9,7 +9,7 @@ from pyas2lib.as2 import Organization from pyas2lib.as2 import Partner -__version__ = "1.3.1" +__version__ = "1.3.2" __all__ = [ diff --git a/setup.py b/setup.py index f178f5e..9ce429d 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,6 @@ "asn1crypto==1.3.0", "oscrypto==1.2.0", "pyOpenSSL==19.1.0", - "dataclasses==0.7;python_version=='3.6'", ] tests_require = [ @@ -25,7 +24,7 @@ long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version="1.3.1", + version="1.3.2", author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages(where=".", exclude=("test*",)), From 2dfa9bcddb9984e712f5af1fd89469c6dc9040a6 Mon Sep 17 00:00:00 2001 From: Noah Katzman Date: Mon, 11 Jan 2021 17:17:16 -0500 Subject: [PATCH 63/66] Upgrade oscrypto to 1.2.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9ce429d..171d144 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ install_requires = [ "asn1crypto==1.3.0", - "oscrypto==1.2.0", + "oscrypto==1.2.1", "pyOpenSSL==19.1.0", ] From a0dce4023e3e58a6ccc6d1973dea3af1692cd260 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 17 Jan 2021 12:17:05 +0530 Subject: [PATCH 64/66] Bump the version of pyas2 to 1.3.3 --- CHANGELOG.md | 3 +++ pyas2lib/__init__.py | 2 +- setup.py | 9 +++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55c6128..96c696b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Release History +## 1.3.3 - 2021-01-17 +* Update the versions of asn1crypto, oscrypto and pyOpenSSL + ## 1.3.2 - 2020-11-01 * Use `signature_algo` attribute when detecting the signature algorithm * Raise exception when unknown `digest_alg` is passed to the sign function diff --git a/pyas2lib/__init__.py b/pyas2lib/__init__.py index fe44dd4..dc799f3 100644 --- a/pyas2lib/__init__.py +++ b/pyas2lib/__init__.py @@ -9,7 +9,7 @@ from pyas2lib.as2 import Organization from pyas2lib.as2 import Partner -__version__ = "1.3.2" +__version__ = "1.3.3" __all__ = [ diff --git a/setup.py b/setup.py index 171d144..2b49fdf 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,14 @@ from setuptools import setup, find_packages install_requires = [ - "asn1crypto==1.3.0", + "asn1crypto==1.4.0", "oscrypto==1.2.1", - "pyOpenSSL==19.1.0", + "pyOpenSSL==20.0.1", ] tests_require = [ - "pytest==6.1.2", + "pytest==6.2.1", + "toml==0.10.1", "pytest-cov==2.8.1", "coverage==5.0.4", "pylint==2.4.4", @@ -24,7 +25,7 @@ long_description="Docs for this project are maintained at " "https://github.com/abhishek-ram/pyas2-lib/blob/" "master/README.md", - version="1.3.2", + version="1.3.3", author="Abhishek Ram", author_email="abhishek8816@gmail.com", packages=find_packages(where=".", exclude=("test*",)), From 602bdee3d238546cd5894d55ee99649ea9cef53b Mon Sep 17 00:00:00 2001 From: elasticdotventures <35611074+elasticdotventures@users.noreply.github.com> Date: Thu, 4 Mar 2021 20:50:25 +1100 Subject: [PATCH 65/66] Caught unhandled Openssl exception when certificate is not valid causes crash & stack dump, regardless of validate_cert setting. --- pyas2lib/utils.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index a76ade9..8e8a67f 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -174,7 +174,10 @@ def split_pem(pem_bytes: bytes): def verify_certificate_chain(cert_bytes, trusted_certs, ignore_self_signed=True): - """Verify a given certificate against a trust store.""" + """ + Verify a given certificate against a trust store. + :return: True; or None if certificate is invalid or cannot be loaded by OpenSSL. + """ # Load the certificate certificate = crypto.load_certificate(crypto.FILETYPE_ASN1, cert_bytes) @@ -194,8 +197,12 @@ def verify_certificate_chain(cert_bytes, trusted_certs, ignore_self_signed=True) store_ctx = crypto.X509StoreContext(store, certificate) # Verify the certificate, returns None if certificate is not valid - store_ctx.verify_certificate() - + try: + store_ctx. + ificate() + except Exception as e: + return None + return True except crypto.X509StoreContextError as e: From 39214ec3ac58360acaf4a65843a0ab43dde84d54 Mon Sep 17 00:00:00 2001 From: elasticdotventures <35611074+elasticdotventures@users.noreply.github.com> Date: Thu, 4 Mar 2021 20:52:39 +1100 Subject: [PATCH 66/66] fixed typo in code --- pyas2lib/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyas2lib/utils.py b/pyas2lib/utils.py index 8e8a67f..8267afd 100644 --- a/pyas2lib/utils.py +++ b/pyas2lib/utils.py @@ -198,8 +198,7 @@ def verify_certificate_chain(cert_bytes, trusted_certs, ignore_self_signed=True) # Verify the certificate, returns None if certificate is not valid try: - store_ctx. - ificate() + store_ctx.verify_certificate() except Exception as e: return None