Skip to content

Commit

Permalink
handle exceptions with parsing signed attributes and more debug logging
Browse files Browse the repository at this point in the history
  • Loading branch information
abhishekram committed Jun 25, 2019
1 parent 58f5523 commit 215d40d
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 84 deletions.
154 changes: 81 additions & 73 deletions pyas2lib/as2.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ def build(self, data, filename=None, subject='AS2 Message',
self.payload = compressed_message

logger.debug(
f'Compressed message {self.message_id} payload as:\n{self.payload.as_string()}')
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
Expand Down Expand Up @@ -415,7 +415,7 @@ def build(self, data, filename=None, subject='AS2 Message',

self.payload = encrypted_message
logger.debug(
f'Encrypted message {self.message_id} payload as:\n{self.payload.as_string()}')
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
Expand All @@ -439,10 +439,11 @@ def build(self, data, filename=None, subject='AS2 Message',
if self.payload.is_multipart():
self.payload.set_boundary(make_mime_boundary())

@staticmethod
def _decompress_data(payload):
def _decompress_data(self, payload):
if payload.get_content_type() == 'application/pkcs7-mime' \
and payload.get_param('smime-type') == 'compressed-data':
logger.debug(f'Decompressing message {self.message_id} payload :\n'
f'{mime_to_bytes(self.payload)}')
compressed_data = payload.get_payload(decode=True)
decompressed_data = decompress_message(compressed_data)
return True, parse_mime(decompressed_data)
Expand Down Expand Up @@ -513,8 +514,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb,

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{self.payload.as_string()}')
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)
Expand All @@ -537,9 +538,8 @@ def parse(self, raw_content, find_org_cb, find_partner_cb,
f'but signed message not found.')

if self.payload.get_content_type() == 'multipart/signed':
logger.debug(
f'Verifying signed message {self.message_id} '
f'payload: \n{self.payload.as_string()}')
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
Expand All @@ -555,8 +555,7 @@ def parse(self, raw_content, find_org_cb, find_partner_cb,
# 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)
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)
Expand Down Expand Up @@ -590,6 +589,9 @@ def parse(self, raw_content, find_org_cb, find_partner_cb,
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)

Expand Down Expand Up @@ -709,14 +711,13 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON
self.payload.attach(mdn_base)

logger.debug(
f'MDN for message {message.message_id} created:\n{mdn_base.as_string()}')
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('-', '')
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)
Expand All @@ -739,8 +740,7 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON
signed_mdn.attach(signature)

self.payload = signed_mdn
logger.debug(
f'Signature for MDN {message.message_id} created:\n{signature.as_string()}')
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():
Expand All @@ -749,6 +749,8 @@ def build(self, message, status, detailed_status=None, confirmation_text=MDN_CON
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 parse(self, raw_content, find_message_cb):
"""Function parses the RAW AS2 MDN, verifies it and extracts the
Expand All @@ -770,68 +772,74 @@ def parse(self, raw_content, find_message_cb):
"""

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)
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

# 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 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')

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
# 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

if self.payload.get_content_type() == 'multipart/signed':
message_boundary = ('--' + self.payload.get_boundary()).encode('utf-8')
# 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)

# 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)
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':
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()
except Exception as e:
status = 'failed/Failure'
detailed_status = f'Failed to parse received MDN. {e}'
logger.error(f'Failed to parse AS2 MDN\n: {traceback.format_exc()}')

for part in self.payload.walk():
if part.get_content_type() == 'message/disposition-notification':
logger.debug(
f'Found 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':
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
finally:
return status, detailed_status

def detect_mdn(self):
""" Function checks if the received raw message is an AS2 MDN or not.
Expand Down
14 changes: 8 additions & 6 deletions pyas2lib/cms.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,22 +343,24 @@ def verify_message(data_to_verify, signature, verify_cert):
cms_content = cms.ContentInfo.load(signature)
digest_alg = None
if cms_content['content_type'].native == 'signed_data':

for signer in cms_content['content']['signer_infos']:

signed_attributes = signer['signed_attrs'].copy()
digest_alg = signer['digest_algorithm']['algorithm'].native

if digest_alg not in DIGEST_ALGORITHMS:
raise Exception('Unsupported Digest Algorithm')

sig_alg = signer['signature_algorithm']['algorithm'].native
sig = signer['signature'].native
signed_data = data_to_verify

if signed_attributes:
if signer['signed_attrs']:
attr_dict = {}
for attr in signed_attributes.native:
attr_dict[attr['type']] = attr['values']
for attr in signer['signed_attrs']:
try:
attr_dict[attr.native['type']] = attr.native['values']
except (ValueError, KeyError):
continue

message_digest = bytes()
for d in attr_dict['message_digest']:
Expand All @@ -371,7 +373,7 @@ def verify_message(data_to_verify, signature, verify_cert):
raise IntegrityError(
'Failed to verify message signature: Message Digest does not match.')

signed_data = signed_attributes.untag().dump()
signed_data = signer['signed_attrs'].untag().dump()

try:
if sig_alg == 'rsassa_pkcs1v15':
Expand Down
32 changes: 32 additions & 0 deletions pyas2lib/tests/test_mdn.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,38 @@ def test_signed_mdn(self):
)
self.assertEqual(status, 'processed')

def test_failed_mdn_parse(self):
"""Test mdn parsing failures are captured."""
# Build an As2 message to be transmitted to partner
self.partner.sign = True
self.partner.encrypt = True
self.partner.mdn_mode = as2.SYNCHRONOUS_MDN
self.partner.mdn_digest_alg = 'sha256'
self.out_message = as2.Message(self.org, self.partner)
self.out_message.build(self.test_data)

# Parse the generated AS2 message as the partner
raw_out_message = \
self.out_message.headers_str + b'\r\n' + self.out_message.content
in_message = as2.Message()
_, _, mdn = in_message.parse(
raw_out_message,
find_org_cb=self.find_org,
find_partner_cb=self.find_partner
)

out_mdn = as2.Mdn()
self.partner.verify_cert = self.mecas2_public_key
self.partner.validate_certs = False
status, detailed_status = out_mdn.parse(
mdn.headers_str + b'\r\n' + mdn.content,
find_message_cb=self.find_message
)
self.assertEqual(status, 'failed/Failure')
self.assertEqual(
detailed_status, 'Failed to parse received MDN. Failed to verify message signature: '
'Message Digest does not match.')

def find_org(self, as2_id):
return self.org

Expand Down
16 changes: 11 additions & 5 deletions pyas2lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,22 @@ def quote_as2name(unquoted_name: str):
class BinaryBytesGenerator(BytesGenerator):
"""Override the bytes generator to better handle binary data."""

def _handle_application_pkcs7_mime(self, msg: email.message.Message):
def _handle_text(self, msg):
"""
Handle writing the binary messages to prevent default behaviour of
newline replacements.
"""
payload = msg.get_payload(decode=True)
if payload is None:
return
if msg.get('Content-Transfer-Encoding') == 'binary' and \
msg.get_content_subtype() in ['pkcs7-mime', 'pkcs7-signature']:
payload = msg.get_payload(decode=True)
if payload is None:
return
else:
self._fp.write(payload)
else:
self._fp.write(payload)
super()._handle_text(msg)

_writeBody = _handle_text


def mime_to_bytes(msg: message.Message, email_policy: policy.Policy = policy.HTTP):
Expand Down

0 comments on commit 215d40d

Please sign in to comment.