def test_encryption(): """Test the encryption and decryption functions.""" with open(os.path.join(TEST_DIR, "cert_test.p12"), "rb") as fp: decrypt_key = Organization.load_key(fp.read(), "test") with open(os.path.join(TEST_DIR, "cert_test_public.pem"), "rb") as fp: encrypt_cert = asymmetric.load_certificate(fp.read()) with pytest.raises(DecryptionError): cms.decrypt_message(INVALID_DATA, None) # Test all the encryption algorithms enc_algorithms = [ "rc2_128_cbc", "rc4_128_cbc", "aes_128_cbc", "aes_192_cbc", "aes_256_cbc", ] for enc_algorithm in enc_algorithms: encrypted_data = cms.encrypt_message(b"data", enc_algorithm, encrypt_cert) _, decrypted_data = cms.decrypt_message(encrypted_data, decrypt_key) assert decrypted_data == b"data" # Test no encryption algorithm with pytest.raises(AS2Exception): cms.encrypt_message(b"data", "rc5_128_cbc", encrypt_cert) # Test no encryption algorithm on decrypt encrypted_data = cms.encrypt_message(b"data", "des_64_cbc", encrypt_cert) with pytest.raises(AS2Exception): cms.decrypt_message(encrypted_data, decrypt_key)
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, 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())