Beispiel #1
0
    def parse(self, raw_content, find_message_cb):
        """Function parses the RAW AS2 MDN, verifies it and extracts the
        processing status of the orginal AS2 message.

        :param raw_content:
            A byte string of the received HTTP headers followed by the body.

        :param find_message_cb:
            A callback the must returns the original Message Object. The
            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
            processing status.
        """

        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)

        # 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

        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
            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)

        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(':')
                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
Beispiel #2
0
    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.

        :param raw_content:
            A byte string of the received HTTP headers followed by the body.

        :param find_org_cb:
            A callback the returns an Organization object if exists. The
            as2-to header value is passed as an argument to it.

        :param 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
            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), 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('>')
            as2_headers[k] = v

        try:
            # Get the organization and partner for this transmission
            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))

            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))

            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(
                    'Incoming messages from partner {} are must be encrypted'
                    ' 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':
                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)

                raw_content = decrypted_content
                self.payload = parse_mime(decrypted_content)

                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')

            # 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(
                    'Incoming messages from partner {} are must be signed '
                    'but signed message not found.'.format(partner_id))

            if self.payload.get_content_type() == 'multipart/signed':
                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
                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)
                    else:
                        self.payload = part

                # 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()
                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)
                digest_func.update(mic_content)
                self.mic = binascii.b2a_base64(digest_func.digest()).strip()

            # 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')
            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())
        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)

            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

                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)

            return status, exception, mdn
Beispiel #3
0
    def build(self, message, status, detailed_status=None):
        """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")

        """

        # 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 = {
            'AS2-Version': AS2_VERSION,
            'ediint-features': EDIINT_FEATURES,
            'Message-ID': '<{}>'.format(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
        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

        # overwrite with partner specific message
        if message.sender and message.sender.mdn_confirm_text:
            confirmation_text = message.sender.mdn_confirm_text

        if status != 'processed':
            confirmation_text = MDN_FAILED_TEXT

        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_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\n'
        mdn_report += 'Original-Recipient: rfc822; {}\n'.format(
            message.headers.get('as2-to'))
        mdn_report += 'Final-Recipient: rfc822; {}\n'.format(
            message.headers.get('as2-to'))
        mdn_report += 'Original-Message-ID: <{}>\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'
        if message.mic:
            mdn_report += 'Received-content-MIC: {}, {}\n'.format(
                message.mic.decode(), message.digest_alg)
        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()))

        # 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('-', '').lower()
            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_payload(
                sign_message(canonicalize(self.payload), self.digest_alg,
                             message.receiver.sign_key))
            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' %
                         (message.message_id, signature.as_string()))

        # Update the headers of the final payload and set message boundary
        for k, v in mdn_headers.items():
            if self.payload.get(k):
                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())
Beispiel #4
0
    def build(self,
              data,
              filename=None,
              subject='AS2 Message',
              content_type='application/edi-consent',
              additional_headers=None):
        """Function builds the AS2 message. Compresses, signs and encrypts
        the payload if applicable.

        :param data: A byte string of the data to be transmitted.

        :param filename:
            Optional filename to be included in the Content-disposition header.

        :param subject:
            The subject for the AS2 message, used by some AS2 servers for
            additional routing of messages. (default "AS2 Message")

        :param content_type:
            The content type for the AS2 message, to be used in the MIME
            header. (default "application/edi-consent")

        :param additional_headers:
            Any additional headers to be included as part of the AS2 message.

        """

        # Validations
        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.')

        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.')

        # Generate message id using UUID 1 as it uses both hostname and time
        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': '<{}>'.format(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),
            # 'recipient-address': message.partner.target_url,
        }
        as2_headers.update(additional_headers)

        # Read the input and convert to bytes if value is unicode/str
        # using utf-8 encoding and finally Canonicalize the payload
        self.payload = email_message.Message()
        self.payload.set_payload(data)
        self.payload.set_type(content_type)
        encoders.encode_7or8bit(self.payload)

        if filename:
            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, 0)))

            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
            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
            mic_content = canonicalize(self.payload)
            digest_func = hashlib.new(self.digest_alg)
            digest_func.update(mic_content)
            self.mic = binascii.b2a_base64(digest_func.digest()).strip()

            # 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_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

            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
            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')
            encrypt_cert = self.receiver.load_encrypt_cert()
            encrypted_message.set_payload(
                encrypt_message(mime_to_bytes(self.payload, 0), self.enc_alg,
                                encrypt_cert))

            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'] = '*****@*****.**'
            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)
            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

        # Update the headers of the final payload and set its boundary
        for k, v in as2_headers.items():
            if self.payload.get(k):
                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())
Beispiel #5
0
    def parse(self, raw_content, find_message_cb):
        """Function parses the RAW AS2 MDN, verifies it and extracts the
        processing status of the orginal AS2 message.

        :param raw_content:
            A byte string of the received HTTP headers followed by the body.

        :param find_message_cb:
            A callback the must returns the original Message Object. The
            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
            processing status.
        """

        status, detailed_status = None, None
        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

            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",
                ]
                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)

            for part in self.payload.walk():
                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":
                        # 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."
                    else:
                        detailed_status = " ".join(mdn_status[1:]).strip()
        except MDNNotFound:
            status = "failed/Failure"
            detailed_status = "mdn-not-found"
        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()}")
        return status, detailed_status
Beispiel #6
0
    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 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
        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",
        }

        # Set the confirmation text message here
        # overwrite with organization specific message
        if message.receiver and message.receiver.mdn_confirm_text:
            confirmation_text = message.receiver.mdn_confirm_text

        # overwrite with partner specific message
        if message.sender and message.sender.mdn_confirm_text:
            confirmation_text = message.sender.mdn_confirm_text

        if status != "processed":
            confirmation_text = failed_text

        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"]
        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_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 += 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_base.set_payload(mdn_report)
        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)}"
        )

        # 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"]
            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"]

            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(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():
            if self.payload.get(k):
                self.payload.replace_header(k, v)
            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)}")
Beispiel #7
0
    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.

        :param raw_content:
            A byte string of the received HTTP headers followed by the body.

        :param find_org_cb:
            A callback the returns an Organization object if exists. The
            as2-to header value is passed as an argument to it.

        :param 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
            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), 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(">")
            as2_headers[k] = v

        try:
            # Get the organization and partner for this transmission
            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}")

            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}")

            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(
                    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)

                self.payload = parse_mime(decrypted_content)

                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")

            # 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(
                    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",
                ]
                for part in self.payload.walk():
                    if part.get_content_type() in signature_types:
                        signature = part.get_payload(decode=True)
                    else:
                        self.payload = part

                # 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()
                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)
                digest_func.update(mic_content)
                self.mic = binascii.b2a_base64(digest_func.digest()).strip()

            # Check for compressed data here
            if not self.compressed:
                self.compressed, self.payload = self._decompress_data(
                    self.payload)

        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()}")

        # 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

            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()

            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
Beispiel #8
0
    def build(
        self,
        data,
        filename=None,
        subject="AS2 Message",
        content_type="application/edi-consent",
        additional_headers=None,
        disposition_notification_to="*****@*****.**",
    ):
        """Function builds the AS2 message. Compresses, signs and encrypts
        the payload if applicable.

        :param data: A byte string of the data to be transmitted.

        :param filename:
            Optional filename to be included in the Content-disposition header.

        :param subject:
            The subject for the AS2 message, used by some AS2 servers for
            additional routing of messages. (default "AS2 Message")

        :param content_type:
            The content type for the AS2 message, to be used in the MIME
            header. (default "application/edi-consent")

        :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 "*****@*****.**")

        """

        # Validations
        assert isinstance(data, bytes), "Parameter data must be of bytes type."

        additional_headers = additional_headers if additional_headers else {}
        assert isinstance(additional_headers, 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."
            )

        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."
            )

        # Generate message id using UUID 1 as it uses both hostname and time
        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_headers.update(additional_headers)

        # Read the input and convert to bytes if value is unicode/str
        # using utf-8 encoding and finally Canonicalize the payload
        self.payload = email_message.Message()
        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"
        else:
            encoders.encode_7or8bit(self.payload)

        if filename:
            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)))
            self.payload = compressed_message

            logger.debug(
                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.attach(self.payload)

            # Calculate the MIC Hash of the message to be verified
            mic_content = canonicalize(self.payload)
            digest_func = hashlib.new(self.digest_alg)
            digest_func.update(mic_content)
            self.mic = binascii.b2a_base64(digest_func.digest()).strip()

            # 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_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(
                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")
            encrypt_cert = self.receiver.load_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(
                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
            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":
                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

        # Update the headers of the final payload and set its boundary
        for k, v in as2_headers.items():
            if self.payload.get(k):
                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())