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 test_signing(): """Test the signing and verification functions.""" with pytest.raises(IntegrityError): cms.verify_message(b'data', INVALID_DATA, None)
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
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
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
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