Пример #1
0
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")
Пример #2
0
def test_signing():
    """Test the signing and verification functions."""
    with pytest.raises(IntegrityError):
        cms.verify_message(b'data', INVALID_DATA, None)
Пример #3
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
Пример #4
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
Пример #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
Пример #6
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