def test_signing(): """Test the signing and verification functions.""" # Load the signature key with open(os.path.join(TEST_DIR, "cert_test.p12"), "rb") as fp: sign_key = Organization.load_key(fp.read(), "test") with open(os.path.join(TEST_DIR, "cert_test_public.pem"), "rb") as fp: verify_cert = asymmetric.load_certificate(fp.read()) # Test failure of signature verification with pytest.raises(IntegrityError): cms.verify_message(b"data", INVALID_DATA, None) # Test signature without signed attributes cms.sign_message(b"data", digest_alg="sha256", sign_key=sign_key, use_signed_attributes=False) # Test pss signature and verification signature = cms.sign_message(b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pss") cms.verify_message(b"data", signature, verify_cert) # Test unsupported signature alg with pytest.raises(AS2Exception): cms.sign_message(b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pssa")
def 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())
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())
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)}")
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())