Пример #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 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())
Пример #3
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())
Пример #4
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)}")
Пример #5
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())