Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async callback functions, partnership function, rsaes_oaep for KeyEncryptionAlgorithm #65

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
999865c
Fix github action build fail due to:
chadgates Jul 22, 2022
85d0b28
Added partner setting to force canonicalize binary.
chadgates Jul 22, 2022
0b3aeae
Formatted with black
chadgates Jul 22, 2022
7c09ba1
Merge pull request #1 from chadgates/fix/broken_build
chadgates Jul 22, 2022
a99c7be
Merge pull request #2 from chadgates/feature/force_binary
chadgates Jul 22, 2022
8add140
Merge remote-tracking branch 'upstream/master'
chadgates Jan 11, 2023
f3d902f
Changes for cargoo
chadgates Jan 11, 2023
47e62fa
Adding Async Support for Callbacks
chadgates Mar 10, 2024
91fe057
Adding Async Support for Callbacks
chadgates Mar 18, 2024
0d88e38
Merge remote-tracking branch 'upstream/master'
chadgates Mar 18, 2024
c2d44c2
Merge branch 'master' into async
chadgates Mar 18, 2024
2e4c253
Add Changelog entries
chadgates Mar 18, 2024
0b39518
Adding key encryption algo option to support rsaes_oaep.
chadgates Mar 18, 2024
43b84e3
Adding key encryption algo option to support rsaes_oaep.
chadgates Mar 18, 2024
e02be38
Adding key encryption algo option to support rsaes_oaep.
chadgates Mar 18, 2024
5dd639f
Formatting
chadgates Mar 18, 2024
c050829
Add pytest-asyncio testing capabilities
chadgates Mar 18, 2024
a93d4c1
Add Async callback to MDN
chadgates Mar 18, 2024
23a8c39
Async Tests
chadgates Mar 18, 2024
c292302
Adding Tests
chadgates Mar 19, 2024
625e479
Adding Tests
chadgates Mar 19, 2024
e362211
Remove obsolete checks
chadgates Mar 19, 2024
cbf0659
https://github.com/abhishek-ram/pyas2-lib/issues/60 and also making h…
chadgates Mar 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AUTHORS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
- Abhishek Ram <[email protected]> @abhishek-ram
- Chad Gates @chadgates
- Wassilios Lytras @chadgates
- Bruno Ribeiro da Silva <[email protected]> @loop0
- Robin C Samuel @robincsamuel
- Brandon Joyce @brandonjoyce
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# Release History

## 1.4.4 - 2024-

* feat: added partnership lookup function
* feat: added support for async callback functions
* feat: added support for optional key encryption algorithm rsaes_oaep for encryption and decryption

## 1.4.3 - 2023-01-25

* fix: update pyopenssl version to resovle pyca/cryptography#7959
* fix: update pyopenssl version to resolve pyca/cryptography#7959

## 1.4.2 - 2022-12-11

Expand Down
137 changes: 119 additions & 18 deletions pyas2lib/as2.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Define the core functions/classes of the pyas2 package."""
import logging
import hashlib
import asyncio
import binascii
import hashlib
import inspect
import logging
import traceback
from dataclasses import dataclass
from email import encoders
from email import message as email_message
from email import message_from_bytes as parse_mime
from email import utils as email_utils
from email.mime.multipart import MIMEMultipart

from oscrypto import asymmetric

from pyas2lib.cms import (
Expand Down Expand Up @@ -179,6 +182,12 @@ class Partner:

:param canonicalize_as_binary: force binary canonicalization for this partner

:param sign_alg: The signing algorithm to be used for generating the
signature. (default `rsassa_pkcs1v15`)

:param key_enc_alg: The key encryption algorithm to be used.
(default `rsaes_pkcs1v15`)

"""

as2_name: str
Expand All @@ -197,6 +206,8 @@ class Partner:
mdn_confirm_text: str = MDN_CONFIRM_TEXT
ignore_self_signed: bool = True
canonicalize_as_binary: bool = False
sign_alg: str = "rsassa_pkcs1v15"
key_enc_alg: str = "rsaes_pkcs1v15"

def __post_init__(self):
"""Run the post initialisation checks for this class."""
Expand Down Expand Up @@ -466,7 +477,10 @@ def build(
)
del signature["MIME-Version"]
signature_data = sign_message(
mic_content, self.digest_alg, self.sender.sign_key
mic_content,
self.digest_alg,
self.sender.sign_key,
self.receiver.sign_alg,
)
signature.set_payload(signature_data)
encoders.encode_base64(signature)
Expand Down Expand Up @@ -539,26 +553,41 @@ def _decompress_data(self, payload):

return False, payload

def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None):
async def aparse(
self,
raw_content,
find_org_cb=None,
find_partner_cb=None,
find_message_cb=None,
find_org_partner_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.
A conditional callback the returns an Organization object if exists. The
as2-to header value is passed as an argument to it. Must be provided
when find_partner_cb is provided and find_org_partner_cb is None

: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.
An conditional callback the returns an Partner object if exists. The
as2-from header value is passed as an argument to it. Must be provided
when find_org_cb is provided and find_org_partner_cb is None.

: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.

:param find_org_partner_cb:
A conditional callback that return Organization object and
Partner object if exist. The as2-to and as2-from header value
are passed as an argument to it. Must be provided
when find_org_cb and find_org_partner_cb is None.

:return:
A three element tuple containing (status, (exception, traceback)
, mdn). The status is a string indicating the status of the
Expand All @@ -567,6 +596,18 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None)
the partner did not request it.
"""

# Validate passed arguments
if not any(
[
find_org_cb and find_partner_cb and not find_org_partner_cb,
find_org_partner_cb and not find_partner_cb and not find_org_cb,
]
):
raise TypeError(
"Incorrect arguments passed: either find_org_cb and find_partner_cb "
"or only find_org_partner_cb must be passed."
)

# 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)
Expand All @@ -580,19 +621,42 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None)
try:
# Get the organization and partner for this transmission
org_id = unquote_as2name(as2_headers["as2-to"])
self.receiver = find_org_cb(org_id)
partner_id = unquote_as2name(as2_headers["as2-from"])

if find_org_partner_cb:
if inspect.iscoroutinefunction(find_org_partner_cb):
self.receiver, self.sender = await find_org_partner_cb(
org_id, partner_id
)
else:
self.receiver, self.sender = find_org_partner_cb(org_id, partner_id)

elif find_org_cb and find_partner_cb:
if inspect.iscoroutinefunction(find_org_cb):
self.receiver = await find_org_cb(org_id)
else:
self.receiver = find_org_cb(org_id)

if inspect.iscoroutinefunction(find_partner_cb):
self.sender = await find_partner_cb(partner_id)
else:
self.sender = find_partner_cb(partner_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 find_message_cb:
if inspect.iscoroutinefunction(find_message_cb):
message_exists = await find_message_cb(self.message_id, partner_id)
else:
message_exists = find_message_cb(self.message_id, partner_id)
if message_exists:
raise DuplicateDocument(
"Duplicate message received, message with this ID already processed."
)

if (
self.sender.encrypt
Expand Down Expand Up @@ -713,6 +777,18 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None)

return status, exception, mdn

def parse(self, *args, **kwargs):
"""
A synchronous wrapper for the asynchronous parse method.
It runs the parse coroutine in an event loop and returns the result.
"""
loop = asyncio.get_event_loop()
if loop.is_running():
raise RuntimeError(
"Cannot run synchronous parse within an already running event loop, use aparse."
)
return loop.run_until_complete(self.aparse(*args, **kwargs))


class Mdn:
"""Class for handling AS2 MDNs. Includes functions for both
Expand Down Expand Up @@ -865,7 +941,10 @@ def build(
del signature["MIME-Version"]

signed_data = sign_message(
canonicalize(self.payload), self.digest_alg, message.receiver.sign_key
canonicalize(self.payload),
self.digest_alg,
message.receiver.sign_key,
message.sender.sign_alg,
)
signature.set_payload(signed_data)
encoders.encode_base64(signature)
Expand All @@ -888,7 +967,7 @@ def build(
f"content:\n {mime_to_bytes(self.payload)}"
)

def parse(self, raw_content, find_message_cb):
async def aparse(self, raw_content, find_message_cb):
"""Function parses the RAW AS2 MDN, verifies it and extracts the
processing status of the orginal AS2 message.

Expand All @@ -913,7 +992,17 @@ def parse(self, raw_content, find_message_cb):
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)
if inspect.iscoroutinefunction(find_message_cb):
orig_message = await find_message_cb(
self.orig_message_id, orig_recipient
)
else:
orig_message = find_message_cb(self.orig_message_id, orig_recipient)

if not orig_message:
status = "failed/Failure"
details_status = "original-message-not-found"
return status, details_status

# Extract the headers and save it
mdn_headers = {}
Expand Down Expand Up @@ -991,6 +1080,18 @@ def parse(self, raw_content, find_message_cb):
logger.error(f"Failed to parse AS2 MDN\n: {traceback.format_exc()}")
return status, detailed_status

def parse(self, *args, **kwargs):
"""
A synchronous wrapper for the asynchronous parse method.
It runs the parse coroutine in an event loop and returns the result.
"""
loop = asyncio.get_event_loop()
if loop.is_running():
raise RuntimeError(
"Cannot run synchronous parse within an already running event loop, use aparse."
)
return loop.run_until_complete(self.aparse(*args, **kwargs))

def detect_mdn(self):
"""Function checks if the received raw message is an AS2 MDN or not.

Expand Down
Loading
Loading