├── pyas2lib ├── tests │ ├── fixtures │ │ ├── cert_test.p12 │ │ ├── payload.binary │ │ ├── sb2bi_signed.mdn │ │ ├── mecas2_compressed.as2 │ │ ├── mecas2_encrypted.as2 │ │ ├── sb2bi_signed_cmp.msg │ │ ├── verify_cert_test2.cer │ │ ├── cert_extract_public.cer │ │ ├── cert_extract_private.cer │ │ ├── mecas2_signed_encrypted.as2 │ │ ├── mecas2_compressed_signed_encrypted.as2 │ │ ├── payload.txt │ │ ├── payload_dos.txt │ │ ├── cert_extract_public.pem │ │ ├── cert_test_public.pem │ │ ├── verify_cert_test1.pem │ │ ├── cert_oldpyas2_public.pem │ │ ├── mecas2_unsigned.mdn │ │ ├── cert_mecas2_public.pem │ │ ├── cert_sb2bi_public.pem │ │ ├── verify_cert_test3.pem │ │ ├── cert_extract_private.pem │ │ ├── cert_oldpyas2_private.pem │ │ ├── cert_test.pem │ │ ├── cert_sb2bi_public.ca │ │ ├── verify_cert_test3.ca │ │ ├── mecas2_signed.mdn │ │ └── mecas2_signed.as2 │ ├── __init__.py │ ├── test_utils.py │ ├── livetest_with_oldpyas2.py │ ├── livetest_with_mecas2.py │ ├── test_with_mecas2.py │ ├── test_mdn.py │ ├── test_cms.py │ ├── test_basic.py │ └── test_advanced.py ├── __init__.py ├── constants.py ├── exceptions.py ├── utils.py └── cms.py ├── AUTHORS.md ├── tox.ini ├── .coveragerc ├── setup.cfg ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── setup.py ├── CHANGELOG.md ├── README.md └── LICENSE /pyas2lib/tests/fixtures/cert_test.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/cert_test.p12 -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/payload.binary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/payload.binary -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/sb2bi_signed.mdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/sb2bi_signed.mdn -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/mecas2_compressed.as2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/mecas2_compressed.as2 -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/mecas2_encrypted.as2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/mecas2_encrypted.as2 -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/sb2bi_signed_cmp.msg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/sb2bi_signed_cmp.msg -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/verify_cert_test2.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/verify_cert_test2.cer -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_extract_public.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/cert_extract_public.cer -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_extract_private.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/cert_extract_private.cer -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/mecas2_signed_encrypted.as2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/mecas2_signed_encrypted.as2 -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/mecas2_compressed_signed_encrypted.as2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishek-ram/pyas2-lib/HEAD/pyas2lib/tests/fixtures/mecas2_compressed_signed_encrypted.as2 -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | - Abhishek Ram @abhishek-ram 2 | - Wassilios Lytras @chadgates 3 | - Bruno Ribeiro da Silva @loop0 4 | - Robin C Samuel @robincsamuel 5 | - Brandon Joyce @brandonjoyce 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py37, py38, py39, py310 8 | 9 | [testenv] 10 | commands = {envpython} setup.py test 11 | deps = 12 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | */site-packages/* 5 | */tests/* 6 | 7 | [report] 8 | exclude_lines = 9 | 10 | # Don't complain about missing debug-only code: 11 | def __repr__ 12 | def __str__ 13 | 14 | # Don't complain if tests don't hit defensive assertion code: 15 | raise AssertionError 16 | raise NotImplementedError 17 | assert 18 | 19 | # Don't complain if non-runnable code isn't run: 20 | if 0: 21 | pass 22 | if __name__ == .__main__.: 23 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [pylama:pycodestyle] 5 | max_line_length = 100 6 | 7 | [pylama:pylint] 8 | max_line_length = 100 9 | ignore = E1101,R0902,R0903,W1203,C0103,C0302,C0209,R0913,R0915,W0612,R1705 10 | 11 | [pylama:pydocstyle] 12 | convention = numpy 13 | ignore = D202 14 | 15 | [pylama:pep8] 16 | max_line_length = 100 17 | 18 | [pylama] 19 | format = pep8 20 | skip = venv/*,.tox/*,*/tests/*,setup.py 21 | linters= pycodestyle,pyflakes,pylint,pep8 22 | ignore = D203,D212,E231,C0330,R0912,R0914,W1202,R1702,C0114,C0302 23 | -------------------------------------------------------------------------------- /pyas2lib/__init__.py: -------------------------------------------------------------------------------- 1 | from pyas2lib.constants import ( 2 | DIGEST_ALGORITHMS, 3 | ENCRYPTION_ALGORITHMS, 4 | MDN_CONFIRM_TEXT, 5 | MDN_FAILED_TEXT, 6 | ) 7 | from pyas2lib.as2 import Mdn 8 | from pyas2lib.as2 import Message 9 | from pyas2lib.as2 import Organization 10 | from pyas2lib.as2 import Partner 11 | 12 | __version__ = "1.4.4" 13 | 14 | 15 | __all__ = [ 16 | "DIGEST_ALGORITHMS", 17 | "ENCRYPTION_ALGORITHMS", 18 | "MDN_CONFIRM_TEXT", 19 | "MDN_FAILED_TEXT", 20 | "Partner", 21 | "Organization", 22 | "Message", 23 | "Mdn", 24 | ] 25 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/payload.txt: -------------------------------------------------------------------------------- 1 | UNB+UNOA:2+:14+:14+140407:0910+5++++1+EANCOM' 2 | UNH+1+ORDERS:D:96A:UN:EAN008' 3 | BGM+220+1AA1TEST+9' 4 | DTM+137:20140407:102' 5 | DTM+63:20140421:102' 6 | DTM+64:20140414:102' 7 | RFF+ADE:1234' 8 | RFF+PD:1704' 9 | NAD+BY+5450534000024::9' 10 | NAD+SU+::9' 11 | NAD+DP+5450534000109::9+++++++GB' 12 | NAD+IV+5450534000055::9++AMAZON EU SARL:5 RUE PLAETIS LUXEMBOURG+CO PO BOX 4558+SLOUGH++SL1 0TX+GB' 13 | RFF+VA:GB727255821' 14 | CUX+2:EUR:9' 15 | LIN+1++9783898307529:EN' 16 | QTY+21:5' 17 | PRI+AAA:27.5' 18 | LIN+2++390787706322:UP' 19 | QTY+21:1' 20 | PRI+AAA:10.87' 21 | LIN+3' 22 | PIA+5+3899408268X-39:SA' 23 | QTY+21:3' 24 | PRI+AAA:3.85' 25 | UNS+S' 26 | CNT+2:3' 27 | UNT+26+1' 28 | UNZ+1+5' 29 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/payload_dos.txt: -------------------------------------------------------------------------------- 1 | UNB+UNOA:2+:14+:14+140407:0910+5++++1+EANCOM' 2 | UNH+1+ORDERS:D:96A:UN:EAN008' 3 | BGM+220+1AA1TEST+9' 4 | DTM+137:20140407:102' 5 | DTM+63:20140421:102' 6 | DTM+64:20140414:102' 7 | RFF+ADE:1234' 8 | RFF+PD:1704' 9 | NAD+BY+5450534000024::9' 10 | NAD+SU+::9' 11 | NAD+DP+5450534000109::9+++++++GB' 12 | NAD+IV+5450534000055::9++AMAZON EU SARL:5 RUE PLAETIS LUXEMBOURG+CO PO BOX 4558+SLOUGH++SL1 0TX+GB' 13 | RFF+VA:GB727255821' 14 | CUX+2:EUR:9' 15 | LIN+1++9783898307529:EN' 16 | QTY+21:5' 17 | PRI+AAA:27.5' 18 | LIN+2++390787706322:UP' 19 | QTY+21:1' 20 | PRI+AAA:10.87' 21 | LIN+3' 22 | PIA+5+3899408268X-39:SA' 23 | QTY+21:3' 24 | PRI+AAA:3.85' 25 | UNS+S' 26 | CNT+2:3' 27 | UNT+26+1' 28 | UNZ+1+5' 29 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: 3 | push: 4 | branches: 5 | - "master" 6 | pull_request: 7 | branches: 8 | - "master" 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | strategy: 13 | matrix: 14 | python-version: [3.7, 3.8, 3.9, "3.10"] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -e ".[tests]" 25 | - name: Run Tests 26 | run: | 27 | pytest --cov=pyas2lib --cov-config .coveragerc --black --pylama 28 | - name: Generate CodeCov Report 29 | run: | 30 | pip install codecov 31 | codecov 32 | -------------------------------------------------------------------------------- /pyas2lib/constants.py: -------------------------------------------------------------------------------- 1 | """Module for defining the constants used by pyas2lib""" 2 | 3 | AS2_VERSION = "1.2" 4 | 5 | EDIINT_FEATURES = "CMS" 6 | 7 | SYNCHRONOUS_MDN = "SYNC" 8 | ASYNCHRONOUS_MDN = "ASYNC" 9 | 10 | MDN_MODES = (SYNCHRONOUS_MDN, ASYNCHRONOUS_MDN) 11 | 12 | MDN_CONFIRM_TEXT = ( 13 | "The AS2 message has been successfully processed. " 14 | "Thank you for exchanging AS2 messages with pyAS2." 15 | ) 16 | 17 | MDN_FAILED_TEXT = ( 18 | "The AS2 message could not be processed. The " 19 | "disposition-notification report has additional details." 20 | ) 21 | 22 | DIGEST_ALGORITHMS = ("md5", "sha1", "sha224", "sha256", "sha384", "sha512") 23 | ENCRYPTION_ALGORITHMS = ( 24 | "tripledes_192_cbc", 25 | "rc2_128_cbc", 26 | "rc4_128_cbc", 27 | "aes_128_cbc", 28 | "aes_192_cbc", 29 | "aes_256_cbc", 30 | ) 31 | SIGNATUR_ALGORITHMS = ( 32 | "rsassa_pkcs1v15", 33 | "rsassa_pss", 34 | ) 35 | KEY_ENCRYPTION_ALGORITHMS = ( 36 | "rsaes_pkcs1v15", 37 | "rsaes_oaep", 38 | ) 39 | -------------------------------------------------------------------------------- /pyas2lib/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures") 5 | 6 | 7 | class Pyas2TestCase(unittest.TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | """Perform the setup actions for the test case.""" 11 | file_list = { 12 | "test_data": "payload.txt", 13 | "test_data_dos": "payload_dos.txt", 14 | "private_key": "cert_test.p12", 15 | "public_key": "cert_test_public.pem", 16 | "mecas2_public_key": "cert_mecas2_public.pem", 17 | "oldpyas2_public_key": "cert_oldpyas2_public.pem", 18 | "oldpyas2_private_key": "cert_oldpyas2_private.pem", 19 | "sb2bi_public_key": "cert_sb2bi_public.pem", 20 | "sb2bi_public_ca": "cert_sb2bi_public.ca", 21 | "private_cer": "cert_extract_private.cer", 22 | "private_pem": "cert_extract_private.pem", 23 | } 24 | 25 | # Load the files to the attrs 26 | for attr, filename in file_list.items(): 27 | with open(os.path.join(TEST_DIR, filename), "rb") as fp: 28 | setattr(cls, attr, fp.read()) 29 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_extract_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBDCCAewCCQC+x6S5XDiB+TANBgkqhkiG9w0BAQsFADBEMQswCQYDVQQGEwJB 3 | VTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwIcHlhczJsaWIxDTALBgNV 4 | BAMMBHRlc3QwHhcNMTkwNjAzMTEzMjU3WhcNMjkwNTMxMTEzMjU3WjBEMQswCQYD 5 | VQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwIcHlhczJsaWIx 6 | DTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/ 7 | ialOlAPsVGq9n3cEhFHBO9G9DyZlket2gVkVk/ONF9fqgRd1uGdrhqqOw0dwjYWH 8 | /heuKF4FbkiNGD9r8iOF2B/Wnj8iEJO0Mc5rKKKmi5e2w/84M9VVYhkpo9AGtb0q 9 | 3COtIqbp5qU7FTqyOsvTvCa13gAVVhHm8naLxCkp6MnL0om2kNK3Exv8rYQybbpe 10 | iLkdZda/3Qo4QEvSS4EKeQsdnN6/W7Rf9GM8gpFXCKykP2tNsESHndIxXrFBPHma 11 | qvA8llncXyUBPJtUFrhb7Q2n+dLT07TmoctOMm+B/Dw6bN7+lHMW44/9xxMCVd/i 12 | muhYicU7rx+bU9bWPNTFAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHfyQ15A/L6A 13 | NZjzwScbmkjnIngjSblxOeTG30Vgcm9f+4T+bLwuF6jd4F5FngkDb/9oE3N3toEk 14 | OwRVtV4mKhiJa5Vn1KGFqDzZ8Hs6GaKaxAFpa8XqPEQx/edVyRmX2S1MFp1qEovu 15 | ldTsYtIC3v2ZmwoxBqPf84974cdmF6j0FrnT/eaBUWCDhjn/XFpL5ZnoDS5JSw5j 16 | E1CpLbNRi4q6fSM+VRPrr1qkGcaXduLUN58B31QizcjCEy2XjAvVSgAq8IoILt3/ 17 | bIUOY/Wbp1BQvVEBxALc34yxWOcUbSamIm6KYSLoMpWOsZAoyuQa2rAXIwAZwejY 18 | 9RfBSn/YWSo= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_test_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDLjCCAhYCCQDQCbI+X/5mlzANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJB 3 | VTETMBEGA1UECAwKU29tZS1TdGF0ZTEiMCAGA1UECgwZSW50ZXJuZXQgV2lkZ2l0 4 | cyBQdHkgTHRkLDERMA8GA1UEAwwIcHlhczJsaWIwHhcNMjIwMTAzMTYwNzI3WhcN 5 | MzIwMTAxMTYwNzI3WjBZMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0 6 | ZTEiMCAGA1UECgwZSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkLDERMA8GA1UEAwwI 7 | cHlhczJsaWIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9GdYaZ2nb 8 | Swv9ciVgnd5YGgKk9MyVgg6qY+gnrarJ2Ii2gMl2CtiaCSIMpVCaq7LRqIXIsGY/ 9 | 7N+/WtESUL8SAmDu+5J5OYWBw5WbrYazpvx9EIRjd3mM0ejzU3U3q/P4D7OhiaJF 10 | kNpc5Tuup/PCtKkdMLvCPITJQkt2dlWrvrR8Kl3CWCJHqLZytOmOKBO+1hleT9XN 11 | waaf3nSc6t9YXq/RUQdUQKkcmiAhqWixyjh4v9o0QFo8qnvIGF0n4i+E1LNaIVSD 12 | Pe6ULkWfpQLc1Ik9QceTyw/yAwUwoI1f016UeQIGCjGoErYCRNtyb74IutETMTcb 13 | oIIEoA1T1SM5AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAA3Gh0CSnfu7RvT37wq 14 | eNWaIeWl3akLbgHAkDoW500D/l6g+zXXYFXZ0bafh0bNV2WKv/lLxc8GW53J2t0k 15 | A/QYUoaWWZBMogResu99eKafZZECcYZdWbXQpvpfwwLq57sPISUko7yEYNWn2DS3 16 | ro26dQmPkyV78x3TXJuQL4ofEVDcB9jYWGIqPZ/n9alBK95ogJF/G6Mq+XYWSYtH 17 | 0He6YW1UASYgtNRHwPLMro6RYlJDsdOWTKkbs3kjroAUtaAj50S3mKjy5J0ERLTn 18 | d+hTtFpCQCoe6m5xGsj3jssuFLT4js5MRzAo7qETcNYkeOCBWkZYOKXH3e5gMqk1 19 | RtI= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/verify_cert_test1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDlzCCAn+gAwIBAgIRAJ/1enBQeC9sg5odhBuWaP4wDQYJKoZIhvcNAQELBQAw 3 | fTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1Nl 4 | YXR0bGUxFzAVBgNVBAoMDkFtYXpvbi5jb20gSW5jMRMwEQYDVQQLDApBbWF6b25M 5 | aW5rMRkwFwYDVQQDDBBOR1dKWl9BUzJfU2VydmVyMB4XDTE4MDIwMzAyNTI0NVoX 6 | DTIzMDIwMjAyNTI0NVowfTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0 7 | b24xEDAOBgNVBAcMB1NlYXR0bGUxFzAVBgNVBAoMDkFtYXpvbi5jb20gSW5jMRMw 8 | EQYDVQQLDApBbWF6b25MaW5rMRkwFwYDVQQDDBBOR1dKWl9BUzJfU2VydmVyMIIB 9 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArEEIBA3We4hV33xdwjEPvese 10 | pcmSxVHDANnOgBsXQHHX6n1HNM25RgMvjbxCK7S9P28F5A4EniJh30EdMx0H7OiV 11 | 4bwoow+sN7DegJh0e7LSaTlP9LtdZnkuZhHKPkUxSbbaFcPeVSFftkmMc9jfl2hX 12 | 907cgFpndIiCMPn6UNwKa98+Nx5UQKhy3KnfIguL3NXJmkd/yiuWx6fL9vLxBfi0 13 | IZzMFY2Hu1BzY3wgzj/DuFoTk4VbH5cqLphAt7GCGeHA+IIfRc/R4RRVqPNnlP56 14 | wELzva3tsv9oGzv94ix8lHhi1bS7uwTVE3uao+C9DYljgqUYUwssSWFC0emz+QID 15 | AQABoxIwEDAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQELBQADggEBADOmJX/D 16 | FBjB1XegoB5HJJ56Tt/b6qUJp2ESZi9a+NFcumc89mY1aAl07c9PqeEWZ3illr8u 17 | 8ehKF7FbXgZr5ihihNqNmmjP7Zyx4ILrY6H5xXmrHXbwCTAlfuDKZd29mZmLD3Wj 18 | uk5Uot3MinmngMEopYxlgKKBq8nq6aBo5TvSKc4DkebPIAm4H73A9kvAosl0YUae 19 | pq4Liv8SyHTEXSZAjWBZbDIOK5sn/npp566mHNFCnZPGvEwHaM0Fhzv6S8edj4kZ 20 | pRK+0IA60A3UbeNm9jyA/cEHV3mxMQsB/FuAfHGhaxUS6g6Hz60jAXL3nOjlbxsk 21 | VGabluFMCoWMkTU= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_oldpyas2_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDtTCCAp2gAwIBAgIJAJ/xkg9bkZxfMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTcwNDA0MDU0ODQwWhcNMTgwNDA0MDU0ODQwWjBF 5 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 7 | CgKCAQEAoX5gXoDG5d6Q0ddvHEiDJDnetYJbkMuE27qSnJRLIGmNb1BAMxckDaUK 8 | pSrEavx6HvFDYNqZtevTMOI2Dr+cS5MNEQUJ0T68wd9QO3QuEAi2gh8FBjTsWlCE 9 | Dop9mRxTCkpdpDse7clOduSXlfBITR8DEdC7DzAomSN1mMccm4QuUdExUbLhAKc2 10 | uvYBiNgyau4rCR0jTjFwFqS4x7Y6GPs5PlOHs3+ZD/lNZ8VZ59SNd30eM6WCQLmb 11 | hmLkuO6EuzTaQFZYbDTsBqkmtJg/Ncm1IGm9NE+57NsZcZT3AxQ6J591E+77Z+GO 12 | /aEE8y/CpezRvfGkPsWQme7eIs7xzwIDAQABo4GnMIGkMB0GA1UdDgQWBBRpPwZ4 13 | VL7mIWCZe8tvhwkaoR3mwzB1BgNVHSMEbjBsgBRpPwZ4VL7mIWCZe8tvhwkaoR3m 14 | w6FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV 15 | BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJ/xkg9bkZxfMAwGA1UdEwQF 16 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAI9vpOsYej9IbyvXebWsXSp1JmdosDli 17 | ECZIA259xMOqjL/Vsdge5BBaUmFvyDDUxfkvj4hQwSBL/LeRwgCPqQ1iz6bEtpJh 18 | 3e0Ds3d8CB5R+rnyeWVQ3jCHvmPKOxnnxfm22m04stlMK4LHOt3mrLfxpelUkhYZ 19 | WZQ2kBMXYs6vLl0nX4NyiMepjdVwPBjCl/zJuXNmTB2Al8cLWkv4P4z5/G78ZNgY 20 | XySEkARXZtQa2qLt0NmhR84yuJdCO4OgS6sMIXzYeXAb476/jRc2yyEsA8q0DSAQ 21 | oIUGEaFwja9Pubas/f00ISXuj+CrcznLjjL2CrXB78QxRYdm1aKulNs= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # IDEA 92 | .idea 93 | .pytest_cache/ 94 | .DS_Store -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/mecas2_unsigned.mdn: -------------------------------------------------------------------------------- 1 | Content-Type: multipart/report; report-type=disposition-notification; 2 | boundary="----=_Part_0_1043598301.1483093396911" 3 | 4 | ------=_Part_0_1043598301.1483093396911 5 | Content-Type: text/plain 6 | Content-Transfer-Encoding: 7bit 7 | 8 | Thank you for exchanging AS2 messages with mendelson opensource AS2. 9 | Please download your free copy of mendelson opensource AS2 + today at http://opensource.mendelson-e-c.com. 10 | 11 | An error occured during the AS2 message processing: Error verifying the senders digital signature: Verification failed 12 | 13 | Signature certificate information: 14 | Serial number (DEC): 10764467591157972577 15 | Issuer: C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=as2server 16 | 17 | Verification certificate information: 18 | Serial number (DEC): 18119724683039678424 19 | Serial number (HEX): FB763198C8751FD8 20 | Finger print (SHA-1): 2E:07:C4:DB:11:BF:76:B2:22:85:B8:8E:7B:34:3E:78:A1:98:BE:ED 21 | Valid from: 11/29/16 22 | Valid to: 11/28/21 23 | Issuer: CN=pyas2lib, O=Internet Widgits Pty Ltd, ST=Some-State, C=AU. 24 | ------=_Part_0_1043598301.1483093396911 25 | Content-Type: message/disposition-notification 26 | Content-Transfer-Encoding: 7bit 27 | 28 | Reporting-UA: mendelson opensource AS2 29 | Original-Recipient: rfc822; mecas2 30 | Final-Recipient: rfc822; mecas2 31 | Original-Message-ID: <20161230102316.10728.85252@imac.local> 32 | Disposition: automatic-action/MDN-sent-automatically; processed/error: authentication-failed 33 | 34 | ------=_Part_0_1043598301.1483093396911-- -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_mecas2_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEJTCCAw2gAwIBAgIEWipbHDANBgkqhkiG9w0BAQsFADCBujEjMCEGCSqGSIb3DQEJARYUc2Vy 3 | dmljZUBtZW5kZWxzb24uZGUxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcM 4 | BkJlcmxpbjEiMCAGA1UECgwZbWVuZGVsc29uLWUtY29tbWVyY2UgR21iSDEhMB8GA1UECwwYRG8g 5 | bm90IHVzZSBpbiBwcm9kdWN0aW9uMR0wGwYDVQQDDBRtZW5kZWxzb24gdGVzdCBrZXkgMzAeFw0x 6 | NzEyMDgwOTI3NTZaFw0yNzEyMDYwOTI3NTZaMIG6MSMwIQYJKoZIhvcNAQkBFhRzZXJ2aWNlQG1l 7 | bmRlbHNvbi5kZTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGlu 8 | MSIwIAYDVQQKDBltZW5kZWxzb24tZS1jb21tZXJjZSBHbWJIMSEwHwYDVQQLDBhEbyBub3QgdXNl 9 | IGluIHByb2R1Y3Rpb24xHTAbBgNVBAMMFG1lbmRlbHNvbiB0ZXN0IGtleSAzMIIBIjANBgkqhkiG 10 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjVOG3wM5krK1Sux4ZFrYgLzf6Ru3RmlCE9UmRyFyJlzUF2aH 11 | F2lq7KW1HISMA0doJARksnHHVeQn5AeItpCwA4ypqItkKXjYGOeR00ZuyiH22qNoVv+pfA9DP3TE 12 | kopx75ux1KFu6/sdATsu7nFaiPyh2Qk6XE4sZ0FL+qRh+1UZDqo1zxVAOC62nBxZPc5I/rg9JPI7 13 | KLrBc8uu1gNTYEAQPUxEDJlGFwnPpm4xeMKWSpQhP3/+QxLnOkWI7awcJFIxeaF/1ug5cyH+4xwf 14 | gLV65sEKKXgzjvHHnpiDc3Fhq2WHQR5gx58D4JbZlAlMMCU7dJDJvQp5dZBFhxE0qQIDAQABozEw 15 | LzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA0GCSqGSIb3 16 | DQEBCwUAA4IBAQBUP3uPOyvSwbJdKJftYOfdEtSFgQJRMFyJb/zY3BhtFW9ylVkze+pQLLjDy4nL 17 | F7nGpa36v5TxXbSSfP3o4NS1Rc4rb5g/hGi0Lx1iyfCYDbCVI5t8NdM8jfxkjb6bYQkja7479N9+ 18 | bCvM8pKFflfEe2sfi4t6qqa4qXYYtVSwTJspD8pgRnMdAS5hd/DkseWwqHEOfLnWiwtAgS8aFyaf 19 | GdZKfToVsLnkFDwbu3gcGhxX+eZz93uuXc9hK9xNYl+DGCv9b3M5S6ynQRP4h4Y6+RXH/MkRHp4C 20 | eH3dWhrG2os1gUfiQYSuFJGJ+SWV1XR9r7jkBo0qm+Gi5PIcExbH 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | install_requires = [ 4 | "asn1crypto==1.5.1", 5 | "oscrypto==1.3.0", 6 | "pyOpenSSL>=23.2.0", 7 | ] 8 | 9 | tests_require = [ 10 | "pytest==6.2.5", 11 | "toml==0.10.2", 12 | "pytest-cov==2.8.1", 13 | "coverage==5.0.4", 14 | "pylint==2.12.1", 15 | "pylama==8.3.7", 16 | "pylama-pylint==3.1.1", 17 | "black==22.6.0", 18 | "pytest-black==0.3.12", 19 | "pyflakes==2.4.0", 20 | ] 21 | 22 | setup( 23 | name="pyas2lib", 24 | description="Python library for building and parsing AS2 Messages", 25 | license="GNU GPL v2.0", 26 | url="https://github.com/abhishek-ram/pyas2-lib", 27 | long_description="Docs for this project are maintained at " 28 | "https://github.com/abhishek-ram/pyas2-lib/blob/" 29 | "master/README.md", 30 | version="1.4.4", 31 | author="Abhishek Ram", 32 | author_email="abhishek8816@gmail.com", 33 | packages=find_packages(where=".", exclude=("test*",)), 34 | classifiers=[ 35 | "Development Status :: 4 - Beta", 36 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 37 | "Intended Audience :: Developers", 38 | "Operating System :: OS Independent", 39 | "Programming Language :: Python", 40 | "Programming Language :: Python :: 3.7", 41 | "Programming Language :: Python :: 3.8", 42 | "Programming Language :: Python :: 3.9", 43 | "Programming Language :: Python :: 3.10", 44 | "Topic :: Security :: Cryptography", 45 | "Topic :: Communications", 46 | ], 47 | setup_requires=["pytest-runner"], 48 | install_requires=install_requires, 49 | tests_require=tests_require, 50 | extras_require={ 51 | "tests": tests_require, 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_sb2bi_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFQzCCBCugAwIBAgIRAPyxGMfr4QFOAAAAAFDb+P8wDQYJKoZIhvcNAQELBQAw 3 | gboxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL 4 | Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg 5 | MjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLjAs 6 | BgNVBAMTJUVudHJ1c3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBMMUswHhcN 7 | MTcwMzI0MTgwODAzWhcNMjAwNjIzMTgzODAxWjBoMQswCQYDVQQGEwJVUzEOMAwG 8 | A1UECBMFVGV4YXMxDzANBgNVBAcTBlRlbXBsZTEdMBsGA1UEChMUTWNMYW5lIENv 9 | bXBhbnksIEluYy4xGTAXBgNVBAMTEGIyYi5tY2xhbmVjby5jb20wggEiMA0GCSqG 10 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDDDv75fHPPyLdBRQkNlkQuk2sKO//abQ+L 11 | mp2xSrHWnnWpTAcuu9H6MQDKPkQKu0mUzNh1WexfmrnutuZeLOmJLTujTuYqp9Hc 12 | p6jKlsQeR9tBe1lVFRNI7L1uHdwiiBFOhViUwsBwWDO1fZ7aUfBXQJrduxeE6O42 13 | 8PaPtk9LP5vsFj6L8MKDskbNee8/v24MPje5e+EesLxmVRESSnC8xV651BQT6lzU 14 | m/bFEzGV8RQLo1Cv6/Qsdb56I2PZYWJyLVItB4h3WvCuZYkwZtzZCShaW4ZV1hGS 15 | IBlHyrvX7CARBTeBWsx0myu7Emvshrk1mSfRwxsm8jTMK/RVP7VVAgMBAAGjggGT 16 | MIIBjzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwMwYDVR0f 17 | BCwwKjAooCagJIYiaHR0cDovL2NybC5lbnRydXN0Lm5ldC9sZXZlbDFrLmNybDBL 18 | BgNVHSAERDBCMDYGCmCGSAGG+mwKAQUwKDAmBggrBgEFBQcCARYaaHR0cDovL3d3 19 | dy5lbnRydXN0Lm5ldC9ycGEwCAYGZ4EMAQICMGgGCCsGAQUFBwEBBFwwWjAjBggr 20 | BgEFBQcwAYYXaHR0cDovL29jc3AuZW50cnVzdC5uZXQwMwYIKwYBBQUHMAKGJ2h0 21 | dHA6Ly9haWEuZW50cnVzdC5uZXQvbDFrLWNoYWluMjU2LmNlcjAxBgNVHREEKjAo 22 | ghBiMmIubWNsYW5lY28uY29tghR3d3cuYjJiLm1jbGFuZWNvLmNvbTAfBgNVHSME 23 | GDAWgBSConB03bxTP8971PfNf6dgxgpMvzAdBgNVHQ4EFgQUiTGHatRioLvWttlv 24 | dXrAH6AKVbIwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAAr7qLcJUTyYc 25 | 9COEI84PTLZx1tRbzZWBzVk3Xa3ucNJY+ifii9LmiU9IZS+Mn+ikN71u7Npj3PBq 26 | rutMS03P/ILObp1Ankil2jVxpQcy1d/VGKSis9IhMmQWzIUqOFfsbZ6SsirQA2Y4 27 | Vz5GGyR6VG37Gn5fZJXKXeottFVVLZkE+/FC+AJJWNGEgt3quo6GENz+3/LgtkUZ 28 | KGlIIBNCrB6/mTTR03MKYiNtxrMgnmcgVtc8W8g1zsXdaTKcpJzZwVKwNuqw6YfE 29 | teP7dKGOKPvqL/i23fHBBKx7DdHRlvVlL04FAK/YuXq06TFOf/AG626j3uxwmRQ2 30 | 7AL3IEu8sA== 31 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/verify_cert_test3.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFQzCCBCugAwIBAgIRAPyxGMfr4QFOAAAAAFDb+P8wDQYJKoZIhvcNAQELBQAw 3 | gboxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL 4 | Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg 5 | MjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLjAs 6 | BgNVBAMTJUVudHJ1c3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBMMUswHhcN 7 | MTcwMzI0MTgwODAzWhcNMjAwNjIzMTgzODAxWjBoMQswCQYDVQQGEwJVUzEOMAwG 8 | A1UECBMFVGV4YXMxDzANBgNVBAcTBlRlbXBsZTEdMBsGA1UEChMUTWNMYW5lIENv 9 | bXBhbnksIEluYy4xGTAXBgNVBAMTEGIyYi5tY2xhbmVjby5jb20wggEiMA0GCSqG 10 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDDDv75fHPPyLdBRQkNlkQuk2sKO//abQ+L 11 | mp2xSrHWnnWpTAcuu9H6MQDKPkQKu0mUzNh1WexfmrnutuZeLOmJLTujTuYqp9Hc 12 | p6jKlsQeR9tBe1lVFRNI7L1uHdwiiBFOhViUwsBwWDO1fZ7aUfBXQJrduxeE6O42 13 | 8PaPtk9LP5vsFj6L8MKDskbNee8/v24MPje5e+EesLxmVRESSnC8xV651BQT6lzU 14 | m/bFEzGV8RQLo1Cv6/Qsdb56I2PZYWJyLVItB4h3WvCuZYkwZtzZCShaW4ZV1hGS 15 | IBlHyrvX7CARBTeBWsx0myu7Emvshrk1mSfRwxsm8jTMK/RVP7VVAgMBAAGjggGT 16 | MIIBjzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwMwYDVR0f 17 | BCwwKjAooCagJIYiaHR0cDovL2NybC5lbnRydXN0Lm5ldC9sZXZlbDFrLmNybDBL 18 | BgNVHSAERDBCMDYGCmCGSAGG+mwKAQUwKDAmBggrBgEFBQcCARYaaHR0cDovL3d3 19 | dy5lbnRydXN0Lm5ldC9ycGEwCAYGZ4EMAQICMGgGCCsGAQUFBwEBBFwwWjAjBggr 20 | BgEFBQcwAYYXaHR0cDovL29jc3AuZW50cnVzdC5uZXQwMwYIKwYBBQUHMAKGJ2h0 21 | dHA6Ly9haWEuZW50cnVzdC5uZXQvbDFrLWNoYWluMjU2LmNlcjAxBgNVHREEKjAo 22 | ghBiMmIubWNsYW5lY28uY29tghR3d3cuYjJiLm1jbGFuZWNvLmNvbTAfBgNVHSME 23 | GDAWgBSConB03bxTP8971PfNf6dgxgpMvzAdBgNVHQ4EFgQUiTGHatRioLvWttlv 24 | dXrAH6AKVbIwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAAr7qLcJUTyYc 25 | 9COEI84PTLZx1tRbzZWBzVk3Xa3ucNJY+ifii9LmiU9IZS+Mn+ikN71u7Npj3PBq 26 | rutMS03P/ILObp1Ankil2jVxpQcy1d/VGKSis9IhMmQWzIUqOFfsbZ6SsirQA2Y4 27 | Vz5GGyR6VG37Gn5fZJXKXeottFVVLZkE+/FC+AJJWNGEgt3quo6GENz+3/LgtkUZ 28 | KGlIIBNCrB6/mTTR03MKYiNtxrMgnmcgVtc8W8g1zsXdaTKcpJzZwVKwNuqw6YfE 29 | teP7dKGOKPvqL/i23fHBBKx7DdHRlvVlL04FAK/YuXq06TFOf/AG626j3uxwmRQ2 30 | 7AL3IEu8sA== 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /pyas2lib/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | __all__ = [ 4 | "ImproperlyConfigured", 5 | "AS2Exception", 6 | "DecompressionError", 7 | "DecryptionError", 8 | "InsufficientSecurityError", 9 | "IntegrityError", 10 | "UnexpectedError", 11 | "MDNNotFound", 12 | "PartnerNotFound", 13 | "DuplicateDocument", 14 | ] 15 | 16 | 17 | class ImproperlyConfigured(Exception): 18 | """ 19 | Exception raised when the config passed to the client is inconsistent 20 | or invalid. 21 | """ 22 | 23 | 24 | class AS2Exception(Exception): 25 | """ 26 | Base class for all exceptions raised by this package's operations (doesn't 27 | apply to :class:`~pyas2lib.ImproperlyConfigured`). 28 | """ 29 | 30 | disposition_type = "failed/Failure" 31 | disposition_modifier = "" 32 | 33 | def __init__(self, message, disposition_modifier=None): 34 | super().__init__(message) 35 | if disposition_modifier: 36 | self.disposition_modifier = disposition_modifier 37 | 38 | 39 | class PartnerNotFound(AS2Exception): 40 | """Raised when the partner/organization for the message could not be found 41 | in the system""" 42 | 43 | disposition_type = "processed/Error" 44 | disposition_modifier = "unknown-trading-partner" 45 | 46 | 47 | class DuplicateDocument(AS2Exception): 48 | """Raised when a message with a duplicate message ID has been received""" 49 | 50 | disposition_type = "processed/Warning" 51 | disposition_modifier = "duplicate-document" 52 | 53 | 54 | class InsufficientSecurityError(AS2Exception): 55 | """Exception raised when the message security is not as per the 56 | settings for the partner.""" 57 | 58 | disposition_type = "processed/Error" 59 | disposition_modifier = "insufficient-message-security" 60 | 61 | 62 | class DecompressionError(AS2Exception): 63 | """Raised when the decompression process fails.""" 64 | 65 | disposition_type = "processed/Error" 66 | disposition_modifier = "decompression-failed" 67 | 68 | 69 | class DecryptionError(AS2Exception): 70 | """Exception raised when decryption process fails.""" 71 | 72 | disposition_type = "processed/Error" 73 | disposition_modifier = "decryption-failed" 74 | 75 | 76 | class IntegrityError(AS2Exception): 77 | """Raised when a signed message signature verification fails""" 78 | 79 | disposition_type = "processed/Error" 80 | disposition_modifier = "authentication-failed" 81 | 82 | 83 | class UnexpectedError(AS2Exception): 84 | """A catch all exception to be raised for any error found while parsing 85 | a received AS2 message""" 86 | 87 | disposition_type = "processed/Error" 88 | disposition_modifier = "unexpected-processing-error" 89 | 90 | 91 | class MDNNotFound(AS2Exception): 92 | """ 93 | Raised when no MDN is found when parsing the received MIME message. 94 | """ 95 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_extract_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIgfSzwLtO4wsCAggA 3 | MB0GCWCGSAFlAwQBKgQQfAIjuBBfqCedJyQh0fvnRASCBNDbGjojvuy02uUpmPCr 4 | sFdlxbAbnodKqxFi7xamh2uuqpl430S7R2QEuCZJUOzjIPPfAhdleVbdU56QIztz 5 | d+RI+/jE8tLWPGeQ0vF1IOeSZqSjPf0ETxiRqrpEJ0KurLnyeul/7nOeEwe/bQAc 6 | kKCDR80d7MZbPQd4L/kZ0uG2VU1tpuVljpkqI778QgOgX08/CGHNKXks3c/Yhfn1 7 | AxRx6eg1y0gazkR9g9wsOAJ69TAjX1hoBlmSZw22nJNZtn8eRlgzWKcKluI0NKFR 8 | V97CA8WKUb59posspV4iyLFiFE+TvO/oMz8CzAoguLHIW5lGX5tQ6HUqbxEHkMbH 9 | 7Hv+6JhQGdHVeGZppcuIeWc/gnCc2X5PWSTx+0c7vBv2FIHM0WjX/krNagzcAL/A 10 | u/6pGv8DrLLQTCSSdCojWJhD6q1VdiXkWnI2uQDHUNFODxywfmUkGROHHF8BcNsA 11 | 4YNKtNCEkNCs3QfoRkLXyRhEL6Rb0k1woR0iz8zK3hiuBTVvu3z6kASoJZJk7isF 12 | DbeekZB2dnrOWZs9HcJ8gNWV61nQg9q67Rf8GzK0DA30r6tFLlWwqmWSzR3QucB/ 13 | ddLXHiq9DyBlSznowYliGo8smHH+oxliMcZ7B8AmmrKwpEHKRGxFR1LkE9PFBIKI 14 | X07PcuZRpynIq2/W+HFSRxFBjUWm7lsykO+ciojbj2caODfKfWs/Ma2kscopghBQ 15 | hqqRzKlsfOxZBWeiJYrqLHZDz4asiYC1gvrc8Krx8u2mZdBodo7T2jfLAtaHMYnR 16 | JwWEhPMq6Ixhc9OnsRVH+KeolthyT0XjR19quqH9mB8oByAhR0eQcmduPoMTwknU 17 | Lah//rckT7sNGvJwum3iGUtIE0y0GBcU/OQ94bHelYKL4kZu//mXvvy+B0eYbqjW 18 | 3C5uy6GhPjBQ6BBMuafu+tfJ/ZGOU3ZG0g/4yrspa/qN5JuDzdMeKax8Y9jQZ5Ba 19 | ZdjCnvr5MWO6krC6evQlkmnag+IOTAfqv+mBtOgZjVS9I49s+6XzR4UNr8dAKMg5 20 | E53dM2gHvg5k80i6JksspONP6+m+rL0ckrB2pYkWrGUyQi/U5f8h2CCChJfLPAEl 21 | PUuhG9Ynh7rGubFtLFe3+RvHWtnIRmg+pxW+W9HBhv26qhkkTlx/AdVeYSoPaldG 22 | 7KsJX/6qJA4EJ4v4QWyyupCYJMmTePx/i4kIFz/CxEDiUc+5BQh08+gafiDvMaNc 23 | 7uiyhBCm3/BVWb9lSem770nz1QawH3Te4fxgKUTlj2ICaQBj37QvkVawjFaawdRq 24 | KvtwagY/B2d8kIjgRWxRbAe/DCv9cpXxdkgsOANZn4S5fn/jmmSpG6t5zQTu55TT 25 | Fkk4uESDIjpUYs65uuNeWZTAuNEiSJON4ha4W8h0/6g3MAjNyJ7YAQgOSSMuSGqk 26 | xdyjWRJ0zmheL2NFYxIAmlrOquMwHMkU8YJb+9jI6RkNq8szkFenIKfCMgb9yfrX 27 | P0aGOz3AvKRlTpcIgY1TDNoGQ0pSlMjW/VmhJJEfeOjbsQdJVr+XObmggXBYv8wE 28 | DJhQXvVSV83iuyq7rCuEEjPs5gpKkq+K5AqXmTtJzFtVMcQ/BmJdVDssPEnjNmL8 29 | visz5I234/hl2utyZOj4yTSiYg== 30 | -----END ENCRYPTED PRIVATE KEY----- 31 | -----BEGIN CERTIFICATE----- 32 | MIIDBDCCAewCCQC+x6S5XDiB+TANBgkqhkiG9w0BAQsFADBEMQswCQYDVQQGEwJB 33 | VTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwIcHlhczJsaWIxDTALBgNV 34 | BAMMBHRlc3QwHhcNMTkwNjAzMTEzMjU3WhcNMjkwNTMxMTEzMjU3WjBEMQswCQYD 35 | VQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwIcHlhczJsaWIx 36 | DTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/ 37 | ialOlAPsVGq9n3cEhFHBO9G9DyZlket2gVkVk/ONF9fqgRd1uGdrhqqOw0dwjYWH 38 | /heuKF4FbkiNGD9r8iOF2B/Wnj8iEJO0Mc5rKKKmi5e2w/84M9VVYhkpo9AGtb0q 39 | 3COtIqbp5qU7FTqyOsvTvCa13gAVVhHm8naLxCkp6MnL0om2kNK3Exv8rYQybbpe 40 | iLkdZda/3Qo4QEvSS4EKeQsdnN6/W7Rf9GM8gpFXCKykP2tNsESHndIxXrFBPHma 41 | qvA8llncXyUBPJtUFrhb7Q2n+dLT07TmoctOMm+B/Dw6bN7+lHMW44/9xxMCVd/i 42 | muhYicU7rx+bU9bWPNTFAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHfyQ15A/L6A 43 | NZjzwScbmkjnIngjSblxOeTG30Vgcm9f+4T+bLwuF6jd4F5FngkDb/9oE3N3toEk 44 | OwRVtV4mKhiJa5Vn1KGFqDzZ8Hs6GaKaxAFpa8XqPEQx/edVyRmX2S1MFp1qEovu 45 | ldTsYtIC3v2ZmwoxBqPf84974cdmF6j0FrnT/eaBUWCDhjn/XFpL5ZnoDS5JSw5j 46 | E1CpLbNRi4q6fSM+VRPrr1qkGcaXduLUN58B31QizcjCEy2XjAvVSgAq8IoILt3/ 47 | bIUOY/Wbp1BQvVEBxALc34yxWOcUbSamIm6KYSLoMpWOsZAoyuQa2rAXIwAZwejY 48 | 9RfBSn/YWSo= 49 | -----END CERTIFICATE----- 50 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_oldpyas2_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIBkTXi4mo7gUCAggA 3 | MB0GCWCGSAFlAwQBKgQQegRDZRY+TL4QgRAWcqdz7wSCBNCGs2pr91erMuGIN8P0 4 | BQPmi4ohcUmhI+q/jtSYIGhkdsV9HTnVncMdr77RO+8xVn9QGpJFxTs+zhOzdsqK 5 | SIcB1EbBjg2OXZ6tKID0Xc2L4ZBGLEV230m995IiiGqPkpF+JYqzcsbVTPFPgs/Y 6 | V4crs3BAJfPw3jgH92++7C5Kvn7hLy8JYbnpGb8mEBoM4QIZ0forKPwOQTQtWQ/W 7 | 82hnEiMEoYIRjdWMAxPIdBBZEsMd55b0jVNxIW/1JZWM+yp9FZNrE7XobzVXUQSo 8 | 6CRqEYvBzJ6+X223bMac8X100zl7NzXxCns8/JN9ts+6x89cqdhCxwjLRlixovvR 9 | tPla9JFFLvie94gd685D5WtKfg6srhNCqr2j8nSIC3FrXyAnOeLJavznxOstey5b 10 | jt1zK6RunZgO9SuBpq5qSr4huUiH7edbMw8WnNhY/ur51GBpjiE4XIVbsPVoeCjz 11 | CL4XgGoJExOSj7n3IvZCTXKxQ9G9M8nk3LYPmIuAEKFduErarwML3GtJsj1r6HbD 12 | BS+WD18BZv4YhhLqPIlwwPszVUNp6NX/AGhs8URGYTTV5iA37doKR+rN6GXVCwDA 13 | Us4xdIdNfVMs/PNDlV4qZ4VrnC2w/6n3N2/K1EwmDPxICaWAtnXBDWMQiXjXPsSW 14 | 8TUIjZMCFgzIqg7PSh4wtIlaz4xmV3EIjyw2oQVc8NRK01TEhW4ZvEi3WEQB16Un 15 | fu24OpNPSDYyeUBbnNQ1bF+Hg2d2d/OiGR8Fma/dW2obhFJVz7CDXt0LeazYR3wZ 16 | 4tIdec4t5cxRb+CTlpdv+4AcvPYC3y4nwDYxUAO1rKQthnFCkrWm/Uy6PxbF83NS 17 | 95/6YuMoKb3bYZQSe566F5t/SL6zEYOFNiLWRbYqtLE1ICoAKus7AhbaLQuLsVW0 18 | /0zYCDVuc5cNt3Lk1MlFc7m8h+jVRroQpipdG+5UslSNYZJIh5QhGPcly+c0vHpa 19 | mDIX3nv7o4SdJAOuEek4XLRb9Ov/2JC3ajkr2NLXiNyH2HUvDGVujLzOE2ZfUozE 20 | u8EjmYY1bTIJuq5+86IRt5QWchs1jWKklgo3PIpZGmUk2CxdgyoA9BHFR9TVQn+j 21 | BxeKDJFbYX5GG/pByFe2EMllF6yN4PQyeEUMFlo0Tv5o0vsh2nwlkUZxpvPfU188 22 | M+YIZ9wv1Wkcv+xjjHCSMuLDFS5Ajc+5as7eE0gwKabmEmj/z8/0b/cyLBfbpOt7 23 | 87lul9b5Se5UJd83rRZE+QjFUzRWxeGJcHrFh6s3eTJIJUlQLhlcbUSowq7hWjti 24 | 0j+YIkGmFdk5QpEXR/+3DYUwXAemoBWz3eXs4NF09mZ/qASpA67xU7tXKMxl+Aoh 25 | HHS89RiTUetdu4Y6GiLtp31D5VsEJvqel0beiS16KK1mayPJjW1FOLcH2X56Ew04 26 | d+2dsFEk/3gGTrHP8MXJqa3kHjc0erdVlIkJBpB91A5VQjUaYXA6V+kMeKKUAZFg 27 | 2bolAGt1i3aCpYtosGEOdrlP7LTcwBi6QgmJs6IBuELnT5F+kyGEdqgwyhToduHS 28 | yQ9e/pyExp8aK3YOqMCSP9yj49aPHkhmnyKNp6pjw1ar3EY3sUcWvhyDhq41sjq6 29 | hPQEpLerIScoJyTuDTg0WfIlIA== 30 | -----END ENCRYPTED PRIVATE KEY----- 31 | -----BEGIN CERTIFICATE----- 32 | MIIDLjCCAhYCCQD7SuhYur9VTTANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJJ 33 | TjETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 34 | cyBQdHkgTHRkMRIwEAYDVQQDDAlhczJzZXJ2ZXIwHhcNMTcxMjIwMDkxNDU3WhcN 35 | MjIxMjE5MDkxNDU3WjBZMQswCQYDVQQGEwJJTjETMBEGA1UECAwKU29tZS1TdGF0 36 | ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlh 37 | czJzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNC0Vq/TJz 38 | Qasi8VqCvMhDRSN68C00J4GbDGxbbIjpXZ9+4wKd7JcPm6HOsVfQK0xxbtox/BGP 39 | 392Hwz2gjn9nEgwuE7wJD0c5zFrRcQlG1avF0CBgFPa1PuSyTrH8JJ+OFK5dO0Su 40 | W5RfaW/CAwp65mVYcGhWKvQ+D7alCvQ8VD50PMj/vw3Wdsry9TQ44VtAz5WUK/pd 41 | AeNbnyMRS4984o4ycUiGWI1hI3xgzFGmLrBEaSBMxTOrifpx1SnE8pRkX3iLO93E 42 | ec0QlNdUdc+mSOSEnEayANuxcQFegFIDajMs+jGVzCu0hDfQx9IfKSwhIc/Ay0v9 43 | ONyTdapqopgVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAESihgqCRSt5v2aIcoUj 44 | BgvX4nZw0RP/rD8cGc3LA/kDLiRne/tZRzCNPPZrEjO3pyBwI1XzaFP6jxcFPVPS 45 | JefBg2KZVoY2sRNaoXru9x8QWVbrdwWaoLg+v6Oj9iwr9CqMMEa9xegEjje87Sga 46 | gSfYasgEWmx0g+l1rbLxkBeUfDKSGRMqi3r9MryV9udTao+4G7H8WFSHThActlxV 47 | BQAHYVMlr6+O16AMwbKwJWIJ5jT8F8pe4aWZWtd0+EzrmP0MxQvEgObD1gk28miQ 48 | 6MkIbx+9vkBytM9LlME1YUWvQfBy8lKt93+FiTDplpwgDau7pTGGqXJen/wYjq+w 49 | Bxo= 50 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_test.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIOP5vfBVfEC8CAggA 3 | MB0GCWCGSAFlAwQBKgQQHpJ6odD/hY89EZA6zTKpMgSCBNAIQoEg8CWHYtL8TQTc 4 | qQPUikli4mlc3XXuyxTiFvgrKdlCJiy48Qyjltfuy6wvHNS8RG4Mr31i6ZCZlLzA 5 | 57O+7q+iud2I3Lto9oJxd59NF46nshpCN3qqObvxRMy1s2STKijw7VpGZBmUVboP 6 | 2GfRJLwPbKLGVk9i6Nz338miHmheq4qqyYOKMzjtwlnrXOnY6a6nSnkqvIzqev3y 7 | k6wILzzzV1iRlzRVcSBj60MilvMD9NFfH9JWlMtVldu7t60S6l0YerlyxdAOqhzn 8 | bVPKpM9MRaLm0vGnBOYT9hiql3e/a+HHLhPdjPLeHqEaUEe3sGBbkOLcuEGAv12/ 9 | RHMcIMYGku8wDuTxeFTK5Isl6W4sjYrBnOUGk4IRE+w/eUys92YnM2LpcVdJg7et 10 | zgLELuN95ZcreQat8Oj9j1laAGY+HyufXvCVo4pLD3UcAgLP9yDFcQwpmWibE62I 11 | GqZoxL4bUMzwfElmFSEFTWS8aXvbnnGbOh57bmeAg4CtmDxQnGEqAs4aY4+i/lEb 12 | bdBwctpE0c03sfSB4Z9OyWZI5H9wG5f22QhDN/UAwKsXILAQF1gkpgwIYFgg5YzW 13 | wHdBzIxLTUZEuG2HvN+aMJdNMPZ6M7kajSmVn0M12HnnVjS3fOmzJilsLN4blNnU 14 | 8i5sT7B10ULg9vVEwlF98k5of5BlSrOpBOeWWiSRx+8WOUouM3/8E/fcIq9A5enW 15 | 4kRbFO2MTpEK7i1gqrfcHjLUElv5h2ARQNt5NZ8qly8cDV+3kb7NsOhaCbw1jKCn 16 | tjtHYHn00Q0xq3/HGCZrDwGci8pKq78GUJnKzSNEob5kAG8Y2YxgXnvq9pA6s1di 17 | l8s/zNKOdaXq1paup1Gbvzx1d6A0xc/S0yy5rCwR+bATnUM/EiNiJpU5XlNw20kb 18 | 2Wm9YVlHFu/PKfWhl5UQm1JT5fCRb29qlg71bcNL+8QI1hilTdDt6BSvAuKnDZWy 19 | ghgQXDtQnFcg/qSuXEKykipH0GW6opLruX3jsPE1WgLgZxMcNgi1oYSEKb/ANRT6 20 | YN/TvcxQtXmoXk5FKyzwFMNlRBO5sPMC5+vgR/Kw690PtHz/PXJRAyQjlDrhcp+t 21 | gfSdw7b2zdrUNN5pVWOdqkFI7s1+jFt0PkZiQ+Fjpic1KHNe5MJGOsfYnqWAWQXz 22 | 1NtktB+ivI7V1MRpTWeHC4VSVZ9T6z3PJGv6eQSHnJqLqNXLl/IBqUB/IDzD01iX 23 | 8ap61xEUhdxKTb7RzpACELCTHrlg1QF/3eVrkhpjmIO4m02Jmbfwcd2FKAefg5J4 24 | eqXBjdYnjwZ61lBfJ+NuE+fSpkmD8oyHHYcj2aDNPEBEBjhGajZXu4FtmDGD7n3c 25 | fo+lOCvBrqWKxwj1bZ/Aueein+0Rx83vnMOSYvkllYfT18XlzsvBWr3/uGC3AFib 26 | 1DRjSaQdZTupBhJ2oMVCC2zRhb49su+kFivfZI3VrsHegFE1g80LM2dhChFfUTO2 27 | Fa6/guAz/ukJQvE/J4DhMXAAVdUBo3IeCL0aB9/AxM+8gNN9Y3cpqgbfBzHpOAUc 28 | T7L5moy8PMXF/IAy1BwNkaqLmmJKRpp0FqbnjUMNyOm18vV8hT3mwIpHLNLCZzpD 29 | M1Z/QvTdrohL6VAbRcmUUkkupw== 30 | -----END ENCRYPTED PRIVATE KEY----- 31 | -----BEGIN CERTIFICATE----- 32 | MIIDLjCCAhYCCQDQCbI+X/5mlzANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJB 33 | VTETMBEGA1UECAwKU29tZS1TdGF0ZTEiMCAGA1UECgwZSW50ZXJuZXQgV2lkZ2l0 34 | cyBQdHkgTHRkLDERMA8GA1UEAwwIcHlhczJsaWIwHhcNMjIwMTAzMTYwNzI3WhcN 35 | MzIwMTAxMTYwNzI3WjBZMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0 36 | ZTEiMCAGA1UECgwZSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkLDERMA8GA1UEAwwI 37 | cHlhczJsaWIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9GdYaZ2nb 38 | Swv9ciVgnd5YGgKk9MyVgg6qY+gnrarJ2Ii2gMl2CtiaCSIMpVCaq7LRqIXIsGY/ 39 | 7N+/WtESUL8SAmDu+5J5OYWBw5WbrYazpvx9EIRjd3mM0ejzU3U3q/P4D7OhiaJF 40 | kNpc5Tuup/PCtKkdMLvCPITJQkt2dlWrvrR8Kl3CWCJHqLZytOmOKBO+1hleT9XN 41 | waaf3nSc6t9YXq/RUQdUQKkcmiAhqWixyjh4v9o0QFo8qnvIGF0n4i+E1LNaIVSD 42 | Pe6ULkWfpQLc1Ik9QceTyw/yAwUwoI1f016UeQIGCjGoErYCRNtyb74IutETMTcb 43 | oIIEoA1T1SM5AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAA3Gh0CSnfu7RvT37wq 44 | eNWaIeWl3akLbgHAkDoW500D/l6g+zXXYFXZ0bafh0bNV2WKv/lLxc8GW53J2t0k 45 | A/QYUoaWWZBMogResu99eKafZZECcYZdWbXQpvpfwwLq57sPISUko7yEYNWn2DS3 46 | ro26dQmPkyV78x3TXJuQL4ofEVDcB9jYWGIqPZ/n9alBK95ogJF/G6Mq+XYWSYtH 47 | 0He6YW1UASYgtNRHwPLMro6RYlJDsdOWTKkbs3kjroAUtaAj50S3mKjy5J0ERLTn 48 | d+hTtFpCQCoe6m5xGsj3jssuFLT4js5MRzAo7qETcNYkeOCBWkZYOKXH3e5gMqk1 49 | RtI= 50 | -----END CERTIFICATE----- 51 | 52 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/cert_sb2bi_public.ca: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC 3 | VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 4 | cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs 5 | IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz 6 | dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy 7 | NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu 8 | dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt 9 | dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 10 | aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj 11 | YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 12 | AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T 13 | RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN 14 | cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW 15 | wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 16 | U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 17 | jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP 18 | BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN 19 | BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ 20 | jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ 21 | Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v 22 | 1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R 23 | nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH 24 | VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== 25 | -----END CERTIFICATE----- 26 | -----BEGIN CERTIFICATE----- 27 | MIIFDjCCA/agAwIBAgIMDulMwwAAAABR03eFMA0GCSqGSIb3DQEBCwUAMIG+MQsw 28 | CQYDVQQGEwJVUzEWMBQGA1UEChMNRW50cnVzdCwgSW5jLjEoMCYGA1UECxMfU2Vl 29 | IHd3dy5lbnRydXN0Lm5ldC9sZWdhbC10ZXJtczE5MDcGA1UECxMwKGMpIDIwMDkg 30 | RW50cnVzdCwgSW5jLiAtIGZvciBhdXRob3JpemVkIHVzZSBvbmx5MTIwMAYDVQQD 31 | EylFbnRydXN0IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjAeFw0x 32 | NTEwMDUxOTEzNTZaFw0zMDEyMDUxOTQzNTZaMIG6MQswCQYDVQQGEwJVUzEWMBQG 33 | A1UEChMNRW50cnVzdCwgSW5jLjEoMCYGA1UECxMfU2VlIHd3dy5lbnRydXN0Lm5l 34 | dC9sZWdhbC10ZXJtczE5MDcGA1UECxMwKGMpIDIwMTIgRW50cnVzdCwgSW5jLiAt 35 | IGZvciBhdXRob3JpemVkIHVzZSBvbmx5MS4wLAYDVQQDEyVFbnRydXN0IENlcnRp 36 | ZmljYXRpb24gQXV0aG9yaXR5IC0gTDFLMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 37 | MIIBCgKCAQEA2j+W0E25L0Tn2zlem1DuXKVh2kFnUwmqAJqOV38pa9vH4SEkqjrQ 38 | jUcj0u1yFvCRIdJdt7hLqIOPt5EyaM/OJZMssn2XyP7BtBe6CZ4DkJN7fEmDImiK 39 | m95HwzGYei59QAvS7z7Tsoyqj0ip/wDoKVgG97aTWpRzJiatWA7lQrjV6nN5ZGhT 40 | JbiEz5R6rgZFDKNrTdDGvuoYpDbwkrK6HIiPOlJ/915tgxyd8B/lw9bdpXiSPbBt 41 | LOrJz5RBGXFEaLpHPATpXbo+8DX3Fbae8i4VHj9HyMg4p3NFXU2wO7GOFyk36t0F 42 | ASK7lDYqjVs1/lMZLwhGwSqzGmIdTivZGwIDAQABo4IBDDCCAQgwDgYDVR0PAQH/ 43 | BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwMwYIKwYBBQUHAQEEJzAlMCMGCCsG 44 | AQUFBzABhhdodHRwOi8vb2NzcC5lbnRydXN0Lm5ldDAwBgNVHR8EKTAnMCWgI6Ah 45 | hh9odHRwOi8vY3JsLmVudHJ1c3QubmV0L2cyY2EuY3JsMDsGA1UdIAQ0MDIwMAYE 46 | VR0gADAoMCYGCCsGAQUFBwIBFhpodHRwOi8vd3d3LmVudHJ1c3QubmV0L3JwYTAd 47 | BgNVHQ4EFgQUgqJwdN28Uz/Pe9T3zX+nYMYKTL8wHwYDVR0jBBgwFoAUanImetAe 48 | 733nO2lR1GyNn5ASZqswDQYJKoZIhvcNAQELBQADggEBADnVjpiDYcgsY9NwHRkw 49 | y/YJrMxp1cncN0HyMg/vdMNY9ngnCTQIlZIv19+4o/0OgemknNM/TWgrFTEKFcxS 50 | BJPok1DD2bHi4Wi3Ogl08TRYCj93mEC45mj/XeTIRsXsgdfJghhcg85x2Ly/rJkC 51 | k9uUmITSnKa1/ly78EqvIazCP0kkZ9Yujs+szGQVGHLlbHfTUqi53Y2sAEo1GdRv 52 | c6N172tkw+CNgxKhiucOhk3YtCAbvmqljEtoZuMrx1gL+1YQ1JH7HdMxWBCMRON1 53 | exCdtTix9qrKgWRs6PLigVWXUX/hwidQosk8WwBD9lu51aX8/wdQQGcHsFXwt35u 54 | Lcw= 55 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/verify_cert_test3.ca: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC 3 | VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 4 | cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs 5 | IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz 6 | dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy 7 | NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu 8 | dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt 9 | dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 10 | aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj 11 | YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 12 | AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T 13 | RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN 14 | cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW 15 | wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 16 | U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 17 | jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP 18 | BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN 19 | BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ 20 | jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ 21 | Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v 22 | 1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R 23 | nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH 24 | VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== 25 | -----END CERTIFICATE----- 26 | 27 | -----BEGIN CERTIFICATE----- 28 | MIIFDjCCA/agAwIBAgIMDulMwwAAAABR03eFMA0GCSqGSIb3DQEBCwUAMIG+MQsw 29 | CQYDVQQGEwJVUzEWMBQGA1UEChMNRW50cnVzdCwgSW5jLjEoMCYGA1UECxMfU2Vl 30 | IHd3dy5lbnRydXN0Lm5ldC9sZWdhbC10ZXJtczE5MDcGA1UECxMwKGMpIDIwMDkg 31 | RW50cnVzdCwgSW5jLiAtIGZvciBhdXRob3JpemVkIHVzZSBvbmx5MTIwMAYDVQQD 32 | EylFbnRydXN0IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjAeFw0x 33 | NTEwMDUxOTEzNTZaFw0zMDEyMDUxOTQzNTZaMIG6MQswCQYDVQQGEwJVUzEWMBQG 34 | A1UEChMNRW50cnVzdCwgSW5jLjEoMCYGA1UECxMfU2VlIHd3dy5lbnRydXN0Lm5l 35 | dC9sZWdhbC10ZXJtczE5MDcGA1UECxMwKGMpIDIwMTIgRW50cnVzdCwgSW5jLiAt 36 | IGZvciBhdXRob3JpemVkIHVzZSBvbmx5MS4wLAYDVQQDEyVFbnRydXN0IENlcnRp 37 | ZmljYXRpb24gQXV0aG9yaXR5IC0gTDFLMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 38 | MIIBCgKCAQEA2j+W0E25L0Tn2zlem1DuXKVh2kFnUwmqAJqOV38pa9vH4SEkqjrQ 39 | jUcj0u1yFvCRIdJdt7hLqIOPt5EyaM/OJZMssn2XyP7BtBe6CZ4DkJN7fEmDImiK 40 | m95HwzGYei59QAvS7z7Tsoyqj0ip/wDoKVgG97aTWpRzJiatWA7lQrjV6nN5ZGhT 41 | JbiEz5R6rgZFDKNrTdDGvuoYpDbwkrK6HIiPOlJ/915tgxyd8B/lw9bdpXiSPbBt 42 | LOrJz5RBGXFEaLpHPATpXbo+8DX3Fbae8i4VHj9HyMg4p3NFXU2wO7GOFyk36t0F 43 | ASK7lDYqjVs1/lMZLwhGwSqzGmIdTivZGwIDAQABo4IBDDCCAQgwDgYDVR0PAQH/ 44 | BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwMwYIKwYBBQUHAQEEJzAlMCMGCCsG 45 | AQUFBzABhhdodHRwOi8vb2NzcC5lbnRydXN0Lm5ldDAwBgNVHR8EKTAnMCWgI6Ah 46 | hh9odHRwOi8vY3JsLmVudHJ1c3QubmV0L2cyY2EuY3JsMDsGA1UdIAQ0MDIwMAYE 47 | VR0gADAoMCYGCCsGAQUFBwIBFhpodHRwOi8vd3d3LmVudHJ1c3QubmV0L3JwYTAd 48 | BgNVHQ4EFgQUgqJwdN28Uz/Pe9T3zX+nYMYKTL8wHwYDVR0jBBgwFoAUanImetAe 49 | 733nO2lR1GyNn5ASZqswDQYJKoZIhvcNAQELBQADggEBADnVjpiDYcgsY9NwHRkw 50 | y/YJrMxp1cncN0HyMg/vdMNY9ngnCTQIlZIv19+4o/0OgemknNM/TWgrFTEKFcxS 51 | BJPok1DD2bHi4Wi3Ogl08TRYCj93mEC45mj/XeTIRsXsgdfJghhcg85x2Ly/rJkC 52 | k9uUmITSnKa1/ly78EqvIazCP0kkZ9Yujs+szGQVGHLlbHfTUqi53Y2sAEo1GdRv 53 | c6N172tkw+CNgxKhiucOhk3YtCAbvmqljEtoZuMrx1gL+1YQ1JH7HdMxWBCMRON1 54 | exCdtTix9qrKgWRs6PLigVWXUX/hwidQosk8WwBD9lu51aX8/wdQQGcHsFXwt35u 55 | Lcw= 56 | -----END CERTIFICATE----- 57 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/mecas2_signed.mdn: -------------------------------------------------------------------------------- 1 | message-id: 2 | content-type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha1; boundary="----=_Part_8_1315897558.1483093497177" 3 | 4 | 5 | ------=_Part_8_1315897558.1483093497177 6 | Content-Type: multipart/report; report-type=disposition-notification; 7 | boundary="----=_Part_5_2083852873.1483093497175" 8 | 9 | ------=_Part_5_2083852873.1483093497175 10 | Content-Type: text/plain 11 | Content-Transfer-Encoding: 7bit 12 | 13 | The AS2 message has been received. Thank you for exchanging AS2 messages with mendelson opensource AS2. 14 | Please download your free copy of mendelson opensource AS2 today at http://opensource.mendelson-e-c.com 15 | 16 | 17 | ------=_Part_5_2083852873.1483093497175 18 | Content-Type: message/disposition-notification 19 | Content-Transfer-Encoding: 7bit 20 | 21 | Reporting-UA: mendelson opensource AS2 22 | Original-Recipient: rfc822; mecas2 23 | Final-Recipient: rfc822; mecas2 24 | Original-Message-ID: <20161230102456.10748.40759@imac.local> 25 | Disposition: automatic-action/MDN-sent-automatically; processed 26 | Received-Content-MIC: O4bvrm5t2YunRfwvZicNdEUmPaPZ9vUslX8loVLDck0=, sha-256 27 | 28 | ------=_Part_5_2083852873.1483093497175-- 29 | 30 | ------=_Part_8_1315897558.1483093497177 31 | Content-Type: application/pkcs7-signature; name=smime.p7s; smime-type=signed-data 32 | Content-Transfer-Encoding: base64 33 | Content-Disposition: attachment; filename="smime.p7s" 34 | Content-Description: S/MIME Cryptographic Signature 35 | 36 | MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIAwggPoMIIC 37 | 0KADAgECAgRXcQQVMA0GCSqGSIb3DQEBBQUAMIG1MS8wLQYJKoZIhvcNAQkBFiB3ZWJtYXN0ZXJA 38 | bWFpbGFkZHJlc3NvbmRvbWFpbi50bzELMAkGA1UEBhMCZGUxEzARBgNVBAgMClN0YXRlIG5hbWUx 39 | FjAUBgNVBAcMDUxvY2FsaXR5IG5hbWUxFTATBgNVBAoMDFlvdXIgY29tcGFueTEgMB4GA1UECwwX 40 | WW91ciBjb21wYW55IGRlcGFydG1lbnQxDzANBgNVBAMMBm1lY2FzMjAeFw0xNjA2MjcxMDQ2NDVa 41 | Fw0xNzA2MjcxMDQ2NDVaMIG1MS8wLQYJKoZIhvcNAQkBFiB3ZWJtYXN0ZXJAbWFpbGFkZHJlc3Nv 42 | bmRvbWFpbi50bzELMAkGA1UEBhMCZGUxEzARBgNVBAgMClN0YXRlIG5hbWUxFjAUBgNVBAcMDUxv 43 | Y2FsaXR5IG5hbWUxFTATBgNVBAoMDFlvdXIgY29tcGFueTEgMB4GA1UECwwXWW91ciBjb21wYW55 44 | IGRlcGFydG1lbnQxDzANBgNVBAMMBm1lY2FzMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 45 | ggEBAJCqCqiYaoyXX6c0c+FgTvn1pFqlmoqc9vvfPlfdhND5KzRaERgWJg8LcN+C/WubGvvezmKk 46 | 5l6kt+w/Mwhafovn/WOZtwyzd1umZhRHthFefqi/gxBkzsdVPMrDAdvVjrHmSdnrNayVPQnZ6n4G 47 | d89CQ9+eEBSXqN1cl75RuOz8Nz8qCNRBzlzsePbcH9a6gUvDt0pDsx6lgKoi0pTZtRXQxkLxQ94C 48 | UuqXoLHEEe2sZeA7wX+ViB1ORSFeptcthtG8OJjcQKRZMaJjTzGjXWldg5O6HTRt7BCGLamvfbcd 49 | 4b7jiDSzCjg/eT6sG2ZTBsfukfOAxn8QFgpHRQ3w95UCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA 50 | GUkqGzlZeIHaiJEPksTkK8mH/ovaBzsTWnoLFCfiduYXoyR4eBudCmSVyZWMAHkRf+HY7wmvX1gg 51 | AhYCmeRixrfb8SGeO/my2F97JGuORK3PjcduXnPxdO+uJRinTqSx+vd+7miDbzFBay3qsxA6IZTI 52 | OlHkK+pw3HbGx1GA32/x0vq1w+Qy0oupNJzDfG0wvCeWlVX/HrVt4xlM65leSV6oG0cjUXdsc5bn 53 | kfZV8IZcu5gXKRHWVLmFUZRbpQR8ruZEPfDbb+40NRRpi+MHO2tvv7lkxIJyc2hrL0RshZWW33vC 54 | dWJOnAmyyDB1Ifgfi8n9Nes8fUGEPNiDkecrlgAAMYICfDCCAngCAQEwgb4wgbUxLzAtBgkqhkiG 55 | 9w0BCQEWIHdlYm1hc3RlckBtYWlsYWRkcmVzc29uZG9tYWluLnRvMQswCQYDVQQGEwJkZTETMBEG 56 | A1UECAwKU3RhdGUgbmFtZTEWMBQGA1UEBwwNTG9jYWxpdHkgbmFtZTEVMBMGA1UECgwMWW91ciBj 57 | b21wYW55MSAwHgYDVQQLDBdZb3VyIGNvbXBhbnkgZGVwYXJ0bWVudDEPMA0GA1UEAwwGbWVjYXMy 58 | AgRXcQQVMAkGBSsOAwIaBQCggZMwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0B 59 | CQUxDxcNMTYxMjMwMTAyNDU3WjAjBgkqhkiG9w0BCQQxFgQUS7j1nnJZHD1Iq89ZwaZizEiSIpAw 60 | NAYJKoZIhvcNAQkPMScwJTAKBggqhkiG9w0DBzAOBggqhkiG9w0DAgICAIAwBwYFKw4DAgcwDQYJ 61 | KoZIhvcNAQEBBQAEggEAjpYnGzuj7fmBIIheWneJp4oP6wfCe9/M1lSX4Q1Q3evIcKY5N3O3VFTC 62 | c16y4Q3NGsZcn+lQWmVY4SM49m92b4FOQcm+OPXvciuzC9YQtR9ZbF6XxSwVUfEtRlR+w2Ej1+YC 63 | qY45Jafw+YE1swkofJpg4be5BJBQ1SjDT19LoY5nE1D39Pw0f1Lyqs3lMdTO0xE0wyvTjQOHy3+N 64 | I2upyPDmjuDpPInFbTp7PAkl4kVaYmC+leAa3KkrAleA5iX82mDwDWj8dYUD0o+7DuXR91hNRpSX 65 | jEhYLgGFnbSYin1TT+JfOQ2m6ATPvT7sE6uQs6j26C7T75EtaC6im/NS1gAAAAAAAA== 66 | ------=_Part_8_1315897558.1483093497177-- 67 | 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release History 2 | 3 | ## 1.4.4 - 2024-05-18 4 | 5 | * feat: add callback option to find partnerships (organization and partner combinations) 6 | * fix: add specific error when MDN received but original message was not found 7 | * feat: add key encryption algorithm parameters to partners with additional support for rsaes_oaep 8 | * feat: add signature algorithm parameter to partners with additional support for rsassa_pss 9 | * feat: add message id parameter to allow user provided message id 10 | * fix: bump pyOpenSSL version to 23.2.0, which bumps cryptography version to 41.0.x which fixes CVE-2023-2650 11 | 12 | ## 1.4.3 - 2023-01-25 13 | 14 | * fix: update pyopenssl version to resolve pyca/cryptography#7959 15 | 16 | ## 1.4.2 - 2022-12-11 17 | 18 | * fix: update the black version to fix github ci pipeline 19 | * feat: added partner setting to force canonicalize binary 20 | * fix: freeze the version of pyflakes to resolve klen/pylama#224 21 | * feat: update the versions of oscrypt and asn1crypto 22 | * fix: Use SMIMECapabilites from asn1crypto instead of the custom class (needed due to asn1crypto upgrade) 23 | 24 | ## 1.4.1 - 2022-02-06 25 | 26 | * fix: freezing pylama version to avoid breaking changes 27 | * feat: option to pass custom domain for AS2 message-id generation 28 | 29 | ## 1.4.0 - 2022-02-06 30 | 31 | * Handle the case where non utf-8 characters are present in the certificate 32 | * Add support for python 3.10 33 | * Move to GitHub actions for running automated tests 34 | * Fix broken tests due to expired certs (#39) 35 | * Preserve content headers on enveloped data (#36) 36 | * When address-type is not specified, only use provided value (#34) 37 | * Normalize digest algorithm to make it more compatible (#32) 38 | 39 | ## 1.3.3 - 2021-01-17 40 | * Update the versions of asn1crypto, oscrypto and pyOpenSSL 41 | 42 | ## 1.3.2 - 2020-11-01 43 | * Use `signature_algo` attribute when detecting the signature algorithm 44 | * Raise exception when unknown `digest_alg` is passed to the sign function 45 | * Add proper support for handling binary messages 46 | * Look for `Final-Recipient` if `Original-Recipient` is not present in the MDN 47 | * Remove support for python 3.6 48 | * Fix linting and change the linter to pylava 49 | 50 | ## 1.3.1 - 2020-04-12 51 | * Use correct format for setting dataclasses requirement for python 3.6 52 | 53 | ## 1.3.0 - 2020-04-05 54 | * Fix and update the SMIME capabilities in the Signed attributes of a signature 55 | * Update the versions of crypto dependencies and related changes 56 | * Use black and pylama as code formatter and linter 57 | * Increase test coverage and add support for python 3.8 58 | 59 | ## 1.2.2 - 2019-06-26 60 | * Handle MDNNotfound correctly when parsing an mdn 61 | 62 | ## 1.2.1 - 2019-06-25 63 | * Handle exceptions raised when parsing signed attributes in a signature https://github.com/abhishek-ram/django-pyas2/issues/13 64 | * Add more debug logs during build and parse 65 | * Catch errors in MDN parsing and handle accordingly 66 | 67 | ## 1.2.0 - 2019-06-12 68 | 69 | * Use f-strings for string formatting. 70 | * Use HTTP email policy for flattening email messages. 71 | * Add proper support for other encryption algos. 72 | * Use dataclasses for organization and partner. 73 | * Remove support for python 3.5. 74 | * Add utility function for extracting info from certificates. 75 | 76 | ## 1.1.1 - 2019-06-03 77 | 78 | * Remove leftover print statement. 79 | * Add utility for extracting public certificate information. 80 | 81 | ## 1.1.0 - 2019-04-30 82 | 83 | * Handle cases where compression is done before signing. 84 | * Add support for additional encryption algorithms. 85 | * Use binary encoding for encryption and signatures. 86 | * Look for `application/x-pkcs7-signature` when verifying signatures. 87 | * Remove support for Python 2. 88 | 89 | ## 1.0.3 - 2018-05-01 90 | 91 | * Remove unnecessary conversions to bytes. 92 | 93 | ## 1.0.2 - 2018-05-01 94 | 95 | * Fix an issue with message decompression. 96 | * Add optional callback for checking duplicate messages in parse 97 | * Add test cases for decompression and duplicate errors 98 | 99 | ## 1.0.1 - 2018-04-22 100 | 101 | * Check for incorrect passphrase when loading the private key. 102 | * Change field name from `as2_id` to `as2_name` in org and partner 103 | * Change name of class from `MDN` to `Mdn` 104 | * Fix couple of validation issues when loading partner 105 | * Return the traceback along with the exception when parsing messages 106 | * Fix the mechanism for loading and validation partner certs 107 | 108 | ## 1.0.0 - 2018-02-15 109 | 110 | * Initial release. 111 | -------------------------------------------------------------------------------- /pyas2lib/tests/fixtures/mecas2_signed.as2: -------------------------------------------------------------------------------- 1 | content-length: 3669 2 | content-type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha256; boundary="----=_Part_211_306083396.1641304626706" 3 | as2-version: 1.2 4 | ediint-features: multiple-attachments, CEM 5 | mime-version: 1.0 6 | recipient-address: http://host.docker.internal:8000/pyas2/as2receive/ 7 | message-id: 8 | as2-from: mecas2 9 | as2-to: pyas2lib 10 | subject: AS2 message 11 | from: as2@company.com 12 | connection: close, TE 13 | date: Tue, 04 Jan 2022 13:57:06 UTC 14 | disposition-notification-to: http://www.company.com:8080/as2/HttpReceiver 15 | content-disposition: attachment; filename="smime.p7m" 16 | host: host.docker.internal:8000 17 | user-agent: mendelson opensource AS2 1.1 build 59 - www.mendelson-e-c.com 18 | expect: 100-continue 19 | 20 | Date: Tue, 4 Jan 2022 13:57:06 +0000 (UTC) 21 | 22 | ------=_Part_211_306083396.1641304626706 23 | Content-Type: application/EDI-Consent 24 | Content-Transfer-Encoding: binary 25 | Content-Disposition: attachment; filename=payload.txt 26 | 27 | UNB+UNOA:2+:14+:14+140407:0910+5++++1+EANCOM' 28 | UNH+1+ORDERS:D:96A:UN:EAN008' 29 | BGM+220+1AA1TEST+9' 30 | DTM+137:20140407:102' 31 | DTM+63:20140421:102' 32 | DTM+64:20140414:102' 33 | RFF+ADE:1234' 34 | RFF+PD:1704' 35 | NAD+BY+5450534000024::9' 36 | NAD+SU+::9' 37 | NAD+DP+5450534000109::9+++++++GB' 38 | NAD+IV+5450534000055::9++AMAZON EU SARL:5 RUE PLAETIS LUXEMBOURG+CO PO BOX 4558+SLOUGH++SL1 0TX+GB' 39 | RFF+VA:GB727255821' 40 | CUX+2:EUR:9' 41 | LIN+1++9783898307529:EN' 42 | QTY+21:5' 43 | PRI+AAA:27.5' 44 | LIN+2++390787706322:UP' 45 | QTY+21:1' 46 | PRI+AAA:10.87' 47 | LIN+3' 48 | PIA+5+3899408268X-39:SA' 49 | QTY+21:3' 50 | PRI+AAA:3.85' 51 | UNS+S' 52 | CNT+2:3' 53 | UNT+26+1' 54 | UNZ+1+5' 55 | 56 | ------=_Part_211_306083396.1641304626706 57 | Content-Type: application/pkcs7-signature; name=smime.p7s; smime-type=signed-data 58 | Content-Transfer-Encoding: base64 59 | Content-Disposition: attachment; filename="smime.p7s" 60 | Content-Description: S/MIME Cryptographic Signature 61 | 62 | MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCAMIIE 63 | JTCCAw2gAwIBAgIEWipbHDANBgkqhkiG9w0BAQsFADCBujEjMCEGCSqGSIb3DQEJARYUc2Vydmlj 64 | ZUBtZW5kZWxzb24uZGUxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJl 65 | cmxpbjEiMCAGA1UECgwZbWVuZGVsc29uLWUtY29tbWVyY2UgR21iSDEhMB8GA1UECwwYRG8gbm90 66 | IHVzZSBpbiBwcm9kdWN0aW9uMR0wGwYDVQQDDBRtZW5kZWxzb24gdGVzdCBrZXkgMzAeFw0xNzEy 67 | MDgwOTI3NTZaFw0yNzEyMDYwOTI3NTZaMIG6MSMwIQYJKoZIhvcNAQkBFhRzZXJ2aWNlQG1lbmRl 68 | bHNvbi5kZTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMSIw 69 | IAYDVQQKDBltZW5kZWxzb24tZS1jb21tZXJjZSBHbWJIMSEwHwYDVQQLDBhEbyBub3QgdXNlIGlu 70 | IHByb2R1Y3Rpb24xHTAbBgNVBAMMFG1lbmRlbHNvbiB0ZXN0IGtleSAzMIIBIjANBgkqhkiG9w0B 71 | AQEFAAOCAQ8AMIIBCgKCAQEAjVOG3wM5krK1Sux4ZFrYgLzf6Ru3RmlCE9UmRyFyJlzUF2aHF2lq 72 | 7KW1HISMA0doJARksnHHVeQn5AeItpCwA4ypqItkKXjYGOeR00ZuyiH22qNoVv+pfA9DP3TEkopx 73 | 75ux1KFu6/sdATsu7nFaiPyh2Qk6XE4sZ0FL+qRh+1UZDqo1zxVAOC62nBxZPc5I/rg9JPI7KLrB 74 | c8uu1gNTYEAQPUxEDJlGFwnPpm4xeMKWSpQhP3/+QxLnOkWI7awcJFIxeaF/1ug5cyH+4xwfgLV6 75 | 5sEKKXgzjvHHnpiDc3Fhq2WHQR5gx58D4JbZlAlMMCU7dJDJvQp5dZBFhxE0qQIDAQABozEwLzAO 76 | BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA0GCSqGSIb3DQEB 77 | CwUAA4IBAQBUP3uPOyvSwbJdKJftYOfdEtSFgQJRMFyJb/zY3BhtFW9ylVkze+pQLLjDy4nLF7nG 78 | pa36v5TxXbSSfP3o4NS1Rc4rb5g/hGi0Lx1iyfCYDbCVI5t8NdM8jfxkjb6bYQkja7479N9+bCvM 79 | 8pKFflfEe2sfi4t6qqa4qXYYtVSwTJspD8pgRnMdAS5hd/DkseWwqHEOfLnWiwtAgS8aFyafGdZK 80 | fToVsLnkFDwbu3gcGhxX+eZz93uuXc9hK9xNYl+DGCv9b3M5S6ynQRP4h4Y6+RXH/MkRHp4CeH3d 81 | WhrG2os1gUfiQYSuFJGJ+SWV1XR9r7jkBo0qm+Gi5PIcExbHAAAxggLAMIICvAIBATCBwzCBujEj 82 | MCEGCSqGSIb3DQEJARYUc2VydmljZUBtZW5kZWxzb24uZGUxCzAJBgNVBAYTAkRFMQ8wDQYDVQQI 83 | DAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEiMCAGA1UECgwZbWVuZGVsc29uLWUtY29tbWVyY2Ug 84 | R21iSDEhMB8GA1UECwwYRG8gbm90IHVzZSBpbiBwcm9kdWN0aW9uMR0wGwYDVQQDDBRtZW5kZWxz 85 | b24gdGVzdCBrZXkgMwIEWipbHDANBglghkgBZQMEAgEFAKCBzjAYBgkqhkiG9w0BCQMxCwYJKoZI 86 | hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMjAxMDQxMzU3MDZaMC0GCSqGSIb3DQEJNDEgMB4wDQYJ 87 | YIZIAWUDBAIBBQChDQYJKoZIhvcNAQELBQAwLwYJKoZIhvcNAQkEMSIEIBuj4bISzhEViRCH8qSI 88 | ekN7J6OnAVFiQQ5MtYoCbwzKMDQGCSqGSIb3DQEJDzEnMCUwCgYIKoZIhvcNAwcwDgYIKoZIhvcN 89 | AwICAgCAMAcGBSsOAwIHMA0GCSqGSIb3DQEBCwUABIIBACdGMMegPJGQixibKOjsMPtYdtqZ/2su 90 | x//gbf4aRHjHKIFdzPZqnIXWQf3b0wMUn52cT4uRMZdXyOu6QSIMPbLTwnA4ixexHTN/CEpeamfV 91 | GTyuufTvgfa6BugDBYu+vzcQWyUqWtTa3FkG+xFKxkt+G69Y5Q4zsdykXAHeLcuBv6dt737q1teu 92 | kbVnr+bbUi3hx6/blW7Ndv4HrIB9zwC3H/1jdSrNsfFQFzLFpW7mwSV6K7V+kU+9jOR+4aD0saQU 93 | q5ZS4CpSvzdjXiBmTMXc3uc7m3rHD7cIxfX5u1Fa4UDzAWXecoI2bpiJsbUz4P/tZx9D0fDvDDZ3 94 | Xhy/njAAAAAAAAA= 95 | ------=_Part_211_306083396.1641304626706-- 96 | -------------------------------------------------------------------------------- /pyas2lib/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """Module to test the utility functions of pyas2lib.""" 2 | import datetime 3 | import os 4 | import pytest 5 | from email.message import Message 6 | 7 | from pyas2lib import utils 8 | from pyas2lib.exceptions import AS2Exception 9 | from pyas2lib.tests import TEST_DIR 10 | 11 | 12 | def test_quoting(): 13 | """Test the function for quoting and as2 name.""" 14 | assert utils.quote_as2name("PYAS2LIB") == "PYAS2LIB" 15 | assert utils.quote_as2name("PYAS2 LIB") == '"PYAS2 LIB"' 16 | 17 | 18 | def test_mime_to_bytes_empty_message(): 19 | """ 20 | It will generate the headers with an empty body 21 | """ 22 | message = Message() 23 | message.set_type("application/pkcs7-mime") 24 | assert ( 25 | utils.mime_to_bytes(message) == b"MIME-Version: 1.0\r\n" 26 | b"Content-Type: application/pkcs7-mime\r\n\r\n" 27 | ) 28 | 29 | 30 | def test_mime_to_bytes_unix_text(): 31 | """ 32 | For non-binary content types, 33 | it converts unix new lines to network newlines. 34 | """ 35 | message = Message() 36 | message.set_type("application/xml") 37 | message.set_payload("Some line.\nAnother line.") 38 | 39 | result = utils.mime_to_bytes(message) 40 | 41 | assert ( 42 | b"MIME-Version: 1.0\r\n" 43 | b"Content-Type: application/xml\r\n" 44 | b"\r\n" 45 | b"Some line.\r\n" 46 | b"Another line." 47 | ) == result 48 | 49 | 50 | def test_mime_to_bytes_octet_stream(): 51 | """ 52 | For binary octet-stream content types, 53 | it does not converts unix new lines to network newlines. 54 | """ 55 | message = Message() 56 | message.set_type("application/octet-stream") 57 | message.set_payload("Some line.\nAnother line.\n") 58 | 59 | result = utils.mime_to_bytes(message) 60 | 61 | assert ( 62 | b"MIME-Version: 1.0\r\n" 63 | b"Content-Type: application/octet-stream\r\n" 64 | b"\r\n" 65 | b"Some line.\n" 66 | b"Another line.\n" 67 | ) == result 68 | 69 | 70 | def test_mime_to_bytes_binary(): 71 | """ 72 | It makes no conversion for binary content encoding. 73 | """ 74 | message = Message() 75 | message.set_type("any/type") 76 | message["Content-Transfer-Encoding"] = "binary" 77 | message.set_payload("Some line.\nAnother line.\n") 78 | 79 | result = utils.mime_to_bytes(message) 80 | 81 | assert ( 82 | b"MIME-Version: 1.0\r\n" 83 | b"Content-Type: any/type\r\n" 84 | b"Content-Transfer-Encoding: binary\r\n" 85 | b"\r\n" 86 | b"Some line.\n" 87 | b"Another line.\n" 88 | ) == result 89 | 90 | 91 | def test_make_boundary(): 92 | """Test the function for creating a boundary for multipart messages.""" 93 | assert utils.make_mime_boundary(text="123456") is not None 94 | 95 | 96 | def test_extract_first_part(): 97 | """Test the function for extracting the first part of a multipart message.""" 98 | message = b"header----first_part\n----second_part\n" 99 | assert utils.extract_first_part(message, b"----") == b"first_part" 100 | 101 | message = b"header----first_part\r\n----second_part\r\n" 102 | assert utils.extract_first_part(message, b"----") == b"first_part" 103 | 104 | 105 | def test_cert_verification(): 106 | """Test the verification of a certificate chain.""" 107 | with open(os.path.join(TEST_DIR, "cert_sb2bi_public.pem"), "rb") as fp: 108 | certificate = utils.pem_to_der(fp.read(), return_multiple=False) 109 | 110 | with pytest.raises(AS2Exception): 111 | utils.verify_certificate_chain( 112 | certificate, trusted_certs=[], ignore_self_signed=False 113 | ) 114 | 115 | 116 | def test_extract_certificate_info(): 117 | """Test case that extracts data from private and public certificates 118 | in PEM or DER format""" 119 | 120 | cert_info = { 121 | "valid_from": datetime.datetime( 122 | 2019, 6, 3, 11, 32, 57, tzinfo=datetime.timezone.utc 123 | ), 124 | "valid_to": datetime.datetime( 125 | 2029, 5, 31, 11, 32, 57, tzinfo=datetime.timezone.utc 126 | ), 127 | "subject": [ 128 | ("C", "AU"), 129 | ("ST", "Some-State"), 130 | ("O", "pyas2lib"), 131 | ("CN", "test"), 132 | ], 133 | "issuer": [ 134 | ("C", "AU"), 135 | ("ST", "Some-State"), 136 | ("O", "pyas2lib"), 137 | ("CN", "test"), 138 | ], 139 | "serial": 13747137503594840569, 140 | } 141 | cert_empty = { 142 | "valid_from": None, 143 | "valid_to": None, 144 | "subject": None, 145 | "issuer": None, 146 | "serial": None, 147 | } 148 | 149 | # compare result of function with cert_info dict. 150 | with open(os.path.join(TEST_DIR, "cert_extract_private.cer"), "rb") as fp: 151 | assert utils.extract_certificate_info(fp.read()) == cert_info 152 | 153 | with open(os.path.join(TEST_DIR, "cert_extract_private.pem"), "rb") as fp: 154 | assert utils.extract_certificate_info(fp.read()) == cert_info 155 | 156 | with open(os.path.join(TEST_DIR, "cert_extract_public.cer"), "rb") as fp: 157 | assert utils.extract_certificate_info(fp.read()) == cert_info 158 | 159 | with open(os.path.join(TEST_DIR, "cert_extract_public.pem"), "rb") as fp: 160 | assert utils.extract_certificate_info(fp.read()) == cert_info 161 | 162 | assert utils.extract_certificate_info(b"") == cert_empty 163 | 164 | 165 | def test_normalize_digest_alg_should_return_lowercase_when_string(): 166 | assert utils.normalize_digest_alg("SHA256") == "sha256" 167 | 168 | 169 | def test_normalize_digest_alg_should_return_same_when_not_string(): 170 | assert utils.normalize_digest_alg(None) is None 171 | -------------------------------------------------------------------------------- /pyas2lib/tests/livetest_with_oldpyas2.py: -------------------------------------------------------------------------------- 1 | """Module for testing with a live old pyas2 server.""" 2 | import os 3 | 4 | import requests 5 | 6 | from pyas2lib import as2 7 | from . import Pyas2TestCase 8 | 9 | TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdata") 10 | 11 | 12 | class LiveTestMecAS2(Pyas2TestCase): 13 | def setUp(self): 14 | self.org = as2.Organization( 15 | as2_name="pyas2lib", 16 | sign_key=self.private_key, 17 | sign_key_pass="test", 18 | decrypt_key=self.private_key, 19 | decrypt_key_pass="test", 20 | ) 21 | 22 | self.partner = as2.Partner( 23 | as2_name="pyas2idev", 24 | verify_cert=self.oldpyas2_public_key, 25 | encrypt_cert=self.oldpyas2_public_key, 26 | mdn_mode=as2.SYNCHRONOUS_MDN, 27 | mdn_digest_alg="sha256", 28 | ) 29 | self.out_message = None 30 | 31 | def test_compressed_message(self): 32 | """Send Unencrypted Unsigned Compressed Message to Mendelson AS2""" 33 | 34 | self.partner.compress = True 35 | self.out_message = as2.Message(self.org, self.partner) 36 | self.out_message.build(self.test_data) 37 | 38 | response = requests.post( 39 | "http://localhost:8080/pyas2/as2receive", 40 | headers=self.out_message.headers, 41 | data=self.out_message.content, 42 | ) 43 | raw_mdn = "" 44 | for k, v in response.headers.items(): 45 | raw_mdn += "{}: {}\n".format(k, v) 46 | 47 | raw_mdn = raw_mdn + "\n" + response.text 48 | 49 | out_mdn = as2.Mdn() 50 | status, detailed_status = out_mdn.parse( 51 | raw_mdn, find_message_cb=self.find_message 52 | ) 53 | self.assertEqual(status, "processed") 54 | 55 | def test_encrypted_message(self): 56 | """Send Encrypted Unsigned Uncompressed Message to Mendelson AS2""" 57 | 58 | self.partner.encrypt = True 59 | self.out_message = as2.Message(self.org, self.partner) 60 | self.out_message.build(self.test_data) 61 | 62 | response = requests.post( 63 | "http://localhost:8080/pyas2/as2receive", 64 | headers=self.out_message.headers, 65 | data=self.out_message.content, 66 | ) 67 | raw_mdn = "" 68 | for k, v in response.headers.items(): 69 | raw_mdn += "{}: {}\n".format(k, v) 70 | 71 | raw_mdn = raw_mdn + "\n" + response.text 72 | 73 | out_mdn = as2.Mdn() 74 | status, detailed_status = out_mdn.parse( 75 | raw_mdn, find_message_cb=self.find_message 76 | ) 77 | self.assertEqual(status, "processed") 78 | 79 | def test_signed_message(self): 80 | """Send Unencrypted Signed Uncompressed Message to Mendelson AS2""" 81 | 82 | self.partner.sign = True 83 | self.out_message = as2.Message(self.org, self.partner) 84 | self.out_message.build(self.test_data) 85 | 86 | response = requests.post( 87 | "http://localhost:8080/pyas2/as2receive", 88 | data=self.out_message.content, 89 | headers=self.out_message.headers, 90 | ) 91 | 92 | raw_mdn = "" 93 | for k, v in response.headers.items(): 94 | raw_mdn += "{}: {}\n".format(k, v) 95 | raw_mdn = raw_mdn + "\n" + response.text 96 | 97 | out_mdn = as2.Mdn() 98 | status, detailed_status = out_mdn.parse( 99 | raw_mdn, find_message_cb=self.find_message 100 | ) 101 | self.assertEqual(status, "processed") 102 | 103 | def test_encrypted_signed_message(self): 104 | """Send Encrypted Signed Uncompressed Message to Mendelson AS2""" 105 | 106 | self.partner.sign = True 107 | self.partner.encrypt = True 108 | self.out_message = as2.Message(self.org, self.partner) 109 | self.out_message.build(self.test_data) 110 | 111 | response = requests.post( 112 | "http://localhost:8080/pyas2/as2receive", 113 | data=self.out_message.content, 114 | headers=self.out_message.headers, 115 | ) 116 | 117 | raw_mdn = "" 118 | for k, v in response.headers.items(): 119 | raw_mdn += "{}: {}\n".format(k, v) 120 | raw_mdn = raw_mdn + "\n" + response.text 121 | 122 | out_mdn = as2.Mdn() 123 | status, detailed_status = out_mdn.parse( 124 | raw_mdn, find_message_cb=self.find_message 125 | ) 126 | self.assertEqual(status, "processed") 127 | 128 | def test_encrypted_signed_compressed_message(self): 129 | """Send Encrypted Signed Compressed Message to Mendelson AS2""" 130 | 131 | self.partner.sign = True 132 | self.partner.encrypt = True 133 | self.partner.compress = True 134 | self.out_message = as2.Message(self.org, self.partner) 135 | self.out_message.build(self.test_data) 136 | 137 | response = requests.post( 138 | "http://localhost:8080/pyas2/as2receive", 139 | data=self.out_message.content, 140 | headers=self.out_message.headers, 141 | ) 142 | 143 | raw_mdn = "" 144 | for k, v in response.headers.items(): 145 | raw_mdn += "{}: {}\n".format(k, v) 146 | raw_mdn = raw_mdn + "\n" + response.text 147 | 148 | out_mdn = as2.Mdn() 149 | status, detailed_status = out_mdn.parse( 150 | raw_mdn, find_message_cb=self.find_message 151 | ) 152 | self.assertEqual(status, "processed") 153 | 154 | def find_org(self, headers): 155 | return self.org 156 | 157 | def find_partner(self, headers): 158 | return self.partner 159 | 160 | def find_message(self, message_id, message_recipient): 161 | return self.out_message 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyas2-lib 2 | 3 | [![pypi package](https://img.shields.io/pypi/v/pyas2lib.svg)](https://pypi.python.org/pypi/pyas2lib/) 4 | [![Run Tests](https://github.com/abhishek-ram/pyas2-lib/actions/workflows/run-tests.yml/badge.svg?branch=master&event=push)](https://github.com/abhishek-ram/pyas2-lib/actions/workflows/run-tests.yml?query=branch%3Amaster++) 5 | [![codecov](https://codecov.io/gh/abhishek-ram/pyas2-lib/branch/master/graph/badge.svg)](https://codecov.io/gh/abhishek-ram/pyas2-lib) 6 | 7 | A pure python library for building and parsing message as part of the AS2 messaging protocol. The message definitions follow the AS2 version 1.2 as defined in the [RFC 4130][1].The library is intended to decouple the message construction/deconstruction from the web server/client implementation. The following functionality is part of this library: 8 | 9 | * Compress, Sign and Encrypt the payload to be transmitted. 10 | * Building the MIME Message from the processed payload. 11 | * Building a signed MDN Messages for a received payload. 12 | * Parsing a received MIME data and identifying if it as a Message or MDN. 13 | * Decompress, Decrypt and Verify Signature of the received payload. 14 | * Verify Signature of the received MDN and extract original message status. 15 | 16 | 17 | ## Basic Usage 18 | 19 | Let us take a look at how we can use this library for building and parsing of AS2 Messages. 20 | 21 | ### Setup 22 | 23 | * First we would need to setup an organization and a partner 24 | ```python 25 | from pyas2lib.as2 import Organization, Partner 26 | 27 | my_org = Organization( 28 | as2_name='my_unique_id', # Unique AS2 Id for this organization 29 | sign_key=b'signature_key_bytes', # PEM/DER encoded private key for signature 30 | sign_key_pass='password', # Password private key for signature 31 | decrypt_key=b'decrypt_key_bytes', # PEM/DER encoded private key for decryption 32 | decrypt_key_pass='password' # Password private key for decryption 33 | ) 34 | 35 | a_partner = Partner( 36 | as2_name='partner_unique_id', # Unique AS2 Id of your partner 37 | sign=True, # Set to true for signing the message 38 | verify_cert=b'verify_cert_bytes', # PEM/DER encoded certificate for verifying partner signatures 39 | encrypt=True, # Set to true for encrypting the message 40 | encrypt_cert=b'encrypt_cert_bytes', # PEM/DER encoded certificate for encrypting messages 41 | mdn_mode='SYNC', # Expect to receive synchronous MDNs from this partner 42 | mdn_digest_alg='sha256' # Expect signed MDNs to be returned by this partner 43 | ) 44 | 45 | ``` 46 | 47 | ### Sending a message to your partner 48 | 49 | * The partner is now setup we can build an AS2 message 50 | ```python 51 | from pyas2lib.as2 import Message 52 | 53 | msg = Message(sender=my_org, receiver=a_partner) 54 | msg.build(b'data_to_transmit') 55 | 56 | ``` 57 | * The message is built and now `msg.content` holds the message body and `message.header` dictionary holds the message headers. These need to be passed to any http library for HTTP POSTing to the partner. 58 | * We expect synchronous MDNs so we need to process the response to our HTTP POST 59 | ```python 60 | from pyas2lib.as2 import Mdn 61 | 62 | msg_mdn = Mdn() # Initialize an Mdn object 63 | 64 | # Call the parse method with the HTTP response headers + content and a function that returns the related `pyas2lib.as2.Messsage` object. 65 | status, detailed_status = msg_mdn.parse(b'response_data_with_headers', find_message_func) 66 | ``` 67 | * We parse the response mdn to get the status and detailed status of the message that was transmitted. 68 | 69 | ### Receiving a message from your partner 70 | 71 | * We need to setup and HTTP server with an endpoint for receiving POST requests fro your partner. 72 | * When a requests is received we need to first check if this is an Async MDN 73 | ```python 74 | from pyas2lib.as2 import Mdn 75 | 76 | msg_mdn = Mdn() # Initialize an Mdn object 77 | # Call the parse method with the HTTP request headers + content and a function the returns the related `pyas2lib.as2.Messsage` object. 78 | status, detailed_status = msg_mdn.parse(request_body, find_message_fumc) 79 | ``` 80 | * If this is an Async MDN it will return the status of the original message. 81 | * In case the request is not an MDN then `pyas2lib.exceptions.MDNNotFound` is raised, which needs to be catched and parse the request as a message. 82 | ```python 83 | from pyas2lib.as2 import Message 84 | 85 | msg = Message() 86 | # Call the parse method with the HTTP request headers + content, a function to return the the related `pyas2lib.as2.Organization` object, a function to return the `pyas2lib.as2.Partner` object and a function to check for duplicates. 87 | status, exception, mdn = msg.parse( 88 | request_body, find_organization, find_partner, check_duplicate_msg) 89 | ``` 90 | * The parse function returns a 3 element tuple; the status of parsing, exception if any raised during parsing and an `pyas2lib.as2.Mdn` object for the message. 91 | * If the `mdn.mdn_mode` is `SYNC` then the `mdn.content` and `mdn.header` must be returned in the response. 92 | * If the `mdn.mdn_mode` is `ASYNC` then the mdn must be saved for later processing. 93 | 94 | ## Contribute 95 | 96 | 1. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. 97 | 1. Fork [the repository][2] on GitHub to start making your changes to the **master** branch (or branch off of it). 98 | 1. Create your feature branch: `git checkout -b my-new-feature` 99 | 1. Commit your changes: `git commit -am 'Add some feature'` 100 | 1. Push to the branch: `git push origin my-new-feature` 101 | 1. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to [AUTHORS][3]. 102 | 103 | [1]: https://www.ietf.org/rfc/rfc4130.txt 104 | [2]: https://github.com/abhishek-ram/pyas2-lib 105 | [3]: https://github.com/abhishek-ram/pyas2-lib/blob/master/AUTHORS.md 106 | -------------------------------------------------------------------------------- /pyas2lib/tests/livetest_with_mecas2.py: -------------------------------------------------------------------------------- 1 | """Module for testing with a live mecas2 server.""" 2 | import os 3 | 4 | import requests 5 | 6 | from pyas2lib import as2 7 | from . import Pyas2TestCase 8 | 9 | TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdata") 10 | 11 | 12 | class LiveTestMecAS2(Pyas2TestCase): 13 | def setUp(self): 14 | self.org = as2.Organization( 15 | as2_name="pyas2lib", 16 | sign_key=self.private_key, 17 | sign_key_pass="test", 18 | decrypt_key=self.private_key, 19 | decrypt_key_pass="test", 20 | ) 21 | 22 | self.partner = as2.Partner( 23 | as2_name="mecas2", 24 | verify_cert=self.mecas2_public_key, 25 | encrypt_cert=self.mecas2_public_key, 26 | mdn_mode=as2.SYNCHRONOUS_MDN, 27 | mdn_digest_alg="sha256", 28 | ) 29 | self.out_message = None 30 | 31 | def test_compressed_message(self): 32 | """Send Unencrypted Unsigned Compressed Message to Mendelson AS2""" 33 | 34 | self.partner.compress = True 35 | self.out_message = as2.Message(self.org, self.partner) 36 | self.out_message.build(self.test_data) 37 | 38 | response = requests.post( 39 | "http://localhost:8080/as2/HttpReceiver", 40 | headers=self.out_message.headers, 41 | data=self.out_message.content, 42 | ) 43 | raw_mdn = "" 44 | for k, v in response.headers.items(): 45 | raw_mdn += "{}: {}\n".format(k, v) 46 | 47 | raw_mdn = (raw_mdn + "\n").encode("utf-8") + response.content 48 | 49 | out_mdn = as2.Mdn() 50 | status, detailed_status = out_mdn.parse( 51 | raw_mdn, find_message_cb=self.find_message 52 | ) 53 | self.assertEqual(status, "processed") 54 | 55 | def test_encrypted_message(self): 56 | """Send Encrypted Unsigned Uncompressed Message to Mendelson AS2""" 57 | 58 | self.partner.encrypt = True 59 | self.out_message = as2.Message(self.org, self.partner) 60 | self.out_message.build(self.test_data) 61 | 62 | response = requests.post( 63 | "http://localhost:8080/as2/HttpReceiver", 64 | headers=self.out_message.headers, 65 | data=self.out_message.content, 66 | ) 67 | raw_mdn = "" 68 | for k, v in response.headers.items(): 69 | raw_mdn += "{}: {}\n".format(k, v) 70 | 71 | raw_mdn = (raw_mdn + "\n").encode("utf-8") + response.content 72 | 73 | out_mdn = as2.Mdn() 74 | status, detailed_status = out_mdn.parse( 75 | raw_mdn, find_message_cb=self.find_message 76 | ) 77 | self.assertEqual(status, "processed") 78 | 79 | def test_signed_message(self): 80 | """Send Unencrypted Signed Uncompressed Message to Mendelson AS2""" 81 | 82 | self.partner.sign = True 83 | self.out_message = as2.Message(self.org, self.partner) 84 | self.out_message.build(self.test_data) 85 | 86 | response = requests.post( 87 | "http://localhost:8080/as2/HttpReceiver", 88 | data=self.out_message.content, 89 | headers=self.out_message.headers, 90 | ) 91 | 92 | raw_mdn = "" 93 | for k, v in response.headers.items(): 94 | raw_mdn += "{}: {}\n".format(k, v) 95 | raw_mdn = (raw_mdn + "\n").encode("utf-8") + response.content 96 | 97 | out_mdn = as2.Mdn() 98 | status, detailed_status = out_mdn.parse( 99 | raw_mdn, find_message_cb=self.find_message 100 | ) 101 | self.assertEqual(status, "processed") 102 | 103 | def test_encrypted_signed_message(self): 104 | """Send Encrypted Signed Uncompressed Message to Mendelson AS2""" 105 | 106 | self.partner.sign = True 107 | self.partner.encrypt = True 108 | self.out_message = as2.Message(self.org, self.partner) 109 | self.out_message.build(self.test_data) 110 | 111 | response = requests.post( 112 | "http://localhost:8080/as2/HttpReceiver", 113 | data=self.out_message.content, 114 | headers=self.out_message.headers, 115 | ) 116 | 117 | raw_mdn = "" 118 | for k, v in response.headers.items(): 119 | raw_mdn += "{}: {}\n".format(k, v) 120 | raw_mdn = (raw_mdn + "\n").encode("utf-8") + response.content 121 | 122 | out_mdn = as2.Mdn() 123 | status, detailed_status = out_mdn.parse( 124 | raw_mdn, find_message_cb=self.find_message 125 | ) 126 | self.assertEqual(status, "processed") 127 | 128 | def test_encrypted_signed_compressed_message(self): 129 | """Send Encrypted Signed Compressed Message to Mendelson AS2""" 130 | 131 | self.partner.sign = True 132 | self.partner.encrypt = True 133 | self.partner.compress = True 134 | self.out_message = as2.Message(self.org, self.partner) 135 | self.out_message.build(self.test_data) 136 | 137 | response = requests.post( 138 | "http://localhost:8080/as2/HttpReceiver", 139 | data=self.out_message.content, 140 | headers=self.out_message.headers, 141 | ) 142 | 143 | raw_mdn = "" 144 | for k, v in response.headers.items(): 145 | raw_mdn += "{}: {}\n".format(k, v) 146 | raw_mdn = (raw_mdn + "\n").encode("utf-8") + response.content 147 | 148 | out_mdn = as2.Mdn() 149 | status, detailed_status = out_mdn.parse( 150 | raw_mdn, find_message_cb=self.find_message 151 | ) 152 | self.assertEqual(status, "processed") 153 | 154 | def find_org(self, headers): 155 | return self.org 156 | 157 | def find_partner(self, headers): 158 | return self.partner 159 | 160 | def find_message(self, message_id, message_recipient): 161 | return self.out_message 162 | -------------------------------------------------------------------------------- /pyas2lib/tests/test_with_mecas2.py: -------------------------------------------------------------------------------- 1 | """Module for testing with files generated by mendelson as2 server.""" 2 | import os 3 | 4 | from pyas2lib import as2 5 | from pyas2lib.tests import Pyas2TestCase, TEST_DIR 6 | 7 | 8 | class TestMecAS2(Pyas2TestCase): 9 | def setUp(self): 10 | self.org = as2.Organization( 11 | as2_name="some_organization", 12 | sign_key=self.private_key, 13 | sign_key_pass="test", 14 | decrypt_key=self.private_key, 15 | decrypt_key_pass="test", 16 | ) 17 | self.partner = as2.Partner( 18 | as2_name="mecas2", 19 | verify_cert=self.mecas2_public_key, 20 | encrypt_cert=self.mecas2_public_key, 21 | validate_certs=False, 22 | ) 23 | 24 | def test_compressed_message(self): 25 | """Test Compressed Message received from Mendelson AS2""" 26 | 27 | # Parse the generated AS2 message as the partner 28 | received_file = os.path.join(TEST_DIR, "mecas2_compressed.as2") 29 | with open(received_file, "rb") as fp: 30 | in_message = as2.Message() 31 | in_message.parse( 32 | fp.read(), find_org_cb=self.find_org, find_partner_cb=self.find_partner 33 | ) 34 | 35 | # Compare the mic contents of the input and output messages 36 | self.assertTrue(in_message.compressed) 37 | self.assertEqual(self.test_data, in_message.content) 38 | 39 | def test_encrypted_message(self): 40 | """Test Encrypted Message received from Mendelson AS2""" 41 | 42 | # Parse the generated AS2 message as the partner 43 | received_file = os.path.join(TEST_DIR, "mecas2_encrypted.as2") 44 | with open(received_file, "rb") as fp: 45 | in_message = as2.Message() 46 | in_message.parse( 47 | fp.read(), find_org_cb=self.find_org, find_partner_cb=self.find_partner 48 | ) 49 | 50 | # Compare the mic contents of the input and output messages 51 | self.assertTrue(in_message.encrypted) 52 | self.assertEqual(in_message.enc_alg, "tripledes_192_cbc") 53 | self.assertEqual(self.test_data, in_message.content) 54 | 55 | def test_signed_message(self): 56 | """Test Unencrypted Signed Uncompressed Message from Mendelson AS2""" 57 | # Parse the generated AS2 message as the partner 58 | received_file = os.path.join(TEST_DIR, "mecas2_signed.as2") 59 | with open(received_file, "rb") as fp: 60 | in_message = as2.Message() 61 | in_message.parse( 62 | fp.read(), find_org_cb=self.find_org, find_partner_cb=self.find_partner 63 | ) 64 | 65 | # Compare the mic contents of the input and output messages 66 | self.assertTrue(in_message.signed) 67 | self.assertEqual(in_message.digest_alg, "sha256") 68 | self.assertEqual(self.test_data, in_message.content) 69 | 70 | def test_encrypted_signed_message(self): 71 | """Test Encrypted Signed Uncompressed Message from Mendelson AS2""" 72 | 73 | # Parse the generated AS2 message as the partner 74 | received_file = os.path.join(TEST_DIR, "mecas2_signed_encrypted.as2") 75 | with open(received_file, "rb") as fp: 76 | in_message = as2.Message() 77 | in_message.parse( 78 | fp.read(), find_org_cb=self.find_org, find_partner_cb=self.find_partner 79 | ) 80 | 81 | # Compare the mic contents of the input and output messages 82 | self.assertTrue(in_message.encrypted) 83 | self.assertEqual(in_message.enc_alg, "tripledes_192_cbc") 84 | self.assertTrue(in_message.signed) 85 | self.assertEqual(in_message.digest_alg, "sha256") 86 | self.assertEqual(self.test_data, in_message.content) 87 | 88 | def test_encrypted_signed_compressed_message(self): 89 | """Test Encrypted Signed Compressed Message from Mendelson AS2""" 90 | 91 | # Parse the generated AS2 message as the partner 92 | received_file = os.path.join(TEST_DIR, "mecas2_compressed_signed_encrypted.as2") 93 | with open(received_file, "rb") as fp: 94 | in_message = as2.Message() 95 | in_message.parse( 96 | fp.read(), find_org_cb=self.find_org, find_partner_cb=self.find_partner 97 | ) 98 | 99 | # Compare the mic contents of the input and output messages 100 | self.assertTrue(in_message.encrypted) 101 | self.assertEqual(in_message.enc_alg, "tripledes_192_cbc") 102 | self.assertTrue(in_message.signed) 103 | self.assertEqual(in_message.digest_alg, "sha256") 104 | self.assertEqual(self.test_data, in_message.content) 105 | 106 | def test_unsigned_mdn(self): 107 | """Test Unsigned MDN received from Mendelson AS2""" 108 | 109 | # Parse the generated AS2 message as the partner 110 | received_file = os.path.join(TEST_DIR, "mecas2_unsigned.mdn") 111 | with open(received_file, "rb") as fp: 112 | in_message = as2.Mdn() 113 | status, detailed_status = in_message.parse( 114 | fp.read(), find_message_cb=self.find_message 115 | ) 116 | 117 | self.assertEqual(status, "processed/error") 118 | self.assertEqual(detailed_status, "authentication-failed") 119 | 120 | def test_signed_mdn(self): 121 | """Test Signed MDN received from Mendelson AS2""" 122 | 123 | # Parse the generated AS2 message as the partner 124 | received_file = os.path.join(TEST_DIR, "mecas2_signed.mdn") 125 | with open(received_file, "rb") as fp: 126 | in_message = as2.Mdn() 127 | in_message.parse(fp.read(), find_message_cb=self.find_message) 128 | 129 | def find_org(self, headers): 130 | return self.org 131 | 132 | def find_partner(self, headers): 133 | return self.partner 134 | 135 | def find_message(self, message_id, message_recipient): 136 | message = as2.Message() 137 | message.sender = self.org 138 | message.receiver = self.partner 139 | message.mic = b"O4bvrm5t2YunRfwvZicNdEUmPaPZ9vUslX8loVLDck0=" 140 | return message 141 | -------------------------------------------------------------------------------- /pyas2lib/tests/test_mdn.py: -------------------------------------------------------------------------------- 1 | """Module for testing the MDN related features of pyas2lib""" 2 | import socket 3 | from pyas2lib import as2 4 | from . import Pyas2TestCase 5 | 6 | 7 | class TestMDN(Pyas2TestCase): 8 | def setUp(self): 9 | 10 | self.org = as2.Organization( 11 | as2_name="some_organization", 12 | sign_key=self.private_key, 13 | sign_key_pass="test", 14 | decrypt_key=self.private_key, 15 | decrypt_key_pass="test", 16 | ) 17 | self.partner = as2.Partner( 18 | as2_name="some_partner", 19 | verify_cert=self.public_key, 20 | encrypt_cert=self.public_key, 21 | ) 22 | self.out_message = None 23 | 24 | def test_unsigned_mdn(self): 25 | """Test unsigned MDN generation and parsing""" 26 | 27 | # Build an As2 message to be transmitted to partner 28 | self.partner.sign = True 29 | self.partner.encrypt = True 30 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 31 | self.out_message = as2.Message(self.org, self.partner) 32 | self.out_message.build(self.test_data) 33 | 34 | # Parse the generated AS2 message as the partner 35 | raw_out_message = ( 36 | self.out_message.headers_str + b"\r\n" + self.out_message.content 37 | ) 38 | in_message = as2.Message() 39 | _, _, mdn = in_message.parse( 40 | raw_out_message, 41 | find_org_cb=self.find_org, 42 | find_partner_cb=self.find_partner, 43 | ) 44 | 45 | out_mdn = as2.Mdn() 46 | status, detailed_status = out_mdn.parse( 47 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 48 | ) 49 | 50 | self.assertEqual(status, "processed") 51 | 52 | def test_signed_mdn(self): 53 | """Test signed MDN generation and parsing""" 54 | 55 | # Build an As2 message to be transmitted to partner 56 | self.partner.sign = True 57 | self.partner.encrypt = True 58 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 59 | self.partner.mdn_digest_alg = "sha256" 60 | self.out_message = as2.Message(self.org, self.partner) 61 | self.out_message.build(self.test_data) 62 | 63 | # Parse the generated AS2 message as the partner 64 | raw_out_message = ( 65 | self.out_message.headers_str + b"\r\n" + self.out_message.content 66 | ) 67 | in_message = as2.Message() 68 | _, _, mdn = in_message.parse( 69 | raw_out_message, 70 | find_org_cb=self.find_org, 71 | find_partner_cb=self.find_partner, 72 | ) 73 | 74 | out_mdn = as2.Mdn() 75 | status, detailed_status = out_mdn.parse( 76 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 77 | ) 78 | self.assertEqual(status, "processed") 79 | 80 | def test_failed_mdn_parse(self): 81 | """Test mdn parsing failures are captured.""" 82 | # Build an As2 message to be transmitted to partner 83 | self.partner.sign = True 84 | self.partner.encrypt = True 85 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 86 | self.partner.mdn_digest_alg = "sha256" 87 | self.out_message = as2.Message(self.org, self.partner) 88 | self.out_message.build(self.test_data) 89 | 90 | # Parse the generated AS2 message as the partner 91 | raw_out_message = ( 92 | self.out_message.headers_str + b"\r\n" + self.out_message.content 93 | ) 94 | in_message = as2.Message() 95 | _, _, mdn = in_message.parse( 96 | raw_out_message, 97 | find_org_cb=self.find_org, 98 | find_partner_cb=self.find_partner, 99 | ) 100 | 101 | out_mdn = as2.Mdn() 102 | self.partner.verify_cert = self.mecas2_public_key 103 | self.partner.validate_certs = False 104 | status, detailed_status = out_mdn.parse( 105 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 106 | ) 107 | self.assertEqual(status, "failed/Failure") 108 | self.assertEqual( 109 | detailed_status, 110 | "Failed to parse received MDN. Failed to verify message signature: " 111 | "Message Digest does not match.", 112 | ) 113 | 114 | def test_mdn_with_domain(self): 115 | """Test MDN generation with an org domain""" 116 | self.org.domain = "example.com" 117 | 118 | # Build an As2 message to be transmitted to partner 119 | self.partner.sign = True 120 | self.partner.encrypt = True 121 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 122 | self.out_message = as2.Message(self.org, self.partner) 123 | self.out_message.build(self.test_data) 124 | 125 | # Parse the generated AS2 message as the partner 126 | raw_out_message = ( 127 | self.out_message.headers_str + b"\r\n" + self.out_message.content 128 | ) 129 | in_message = as2.Message() 130 | _, _, mdn = in_message.parse( 131 | raw_out_message, 132 | find_org_cb=self.find_org, 133 | find_partner_cb=self.find_partner, 134 | ) 135 | 136 | out_mdn = as2.Mdn() 137 | status, detailed_status = out_mdn.parse( 138 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 139 | ) 140 | 141 | self.assertEqual(out_mdn.message_id.split("@")[1], self.org.domain) 142 | 143 | def test_mdn_without_domain(self): 144 | """Test MDN generation without an org domain""" 145 | 146 | # Build an As2 message to be transmitted to partner 147 | self.partner.sign = True 148 | self.partner.encrypt = True 149 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 150 | self.out_message = as2.Message(self.org, self.partner) 151 | self.out_message.build(self.test_data) 152 | 153 | # Parse the generated AS2 message as the partner 154 | raw_out_message = ( 155 | self.out_message.headers_str + b"\r\n" + self.out_message.content 156 | ) 157 | in_message = as2.Message() 158 | _, _, mdn = in_message.parse( 159 | raw_out_message, 160 | find_org_cb=self.find_org, 161 | find_partner_cb=self.find_partner, 162 | ) 163 | 164 | out_mdn = as2.Mdn() 165 | status, detailed_status = out_mdn.parse( 166 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 167 | ) 168 | 169 | self.assertEqual(out_mdn.message_id.split("@")[1], socket.getfqdn()) 170 | 171 | def find_org(self, as2_id): 172 | return self.org 173 | 174 | def find_partner(self, as2_id): 175 | return self.partner 176 | 177 | def find_message(self, message_id, message_recipient): 178 | return self.out_message 179 | -------------------------------------------------------------------------------- /pyas2lib/tests/test_cms.py: -------------------------------------------------------------------------------- 1 | """Module to test cms related features of pyas2lib.""" 2 | import os 3 | 4 | import pytest 5 | from oscrypto import asymmetric, symmetric, util 6 | 7 | from asn1crypto import algos, cms as crypto_cms, core 8 | 9 | from pyas2lib.as2 import Organization 10 | from pyas2lib import cms 11 | from pyas2lib.exceptions import ( 12 | AS2Exception, 13 | DecompressionError, 14 | DecryptionError, 15 | IntegrityError, 16 | ) 17 | from pyas2lib.tests import TEST_DIR 18 | 19 | 20 | INVALID_DATA = cms.cms.ContentInfo( 21 | { 22 | "content_type": cms.cms.ContentType("data"), 23 | } 24 | ).dump() 25 | 26 | 27 | def _encrypted_data_with_faulty_key_algo(): 28 | with open(os.path.join(TEST_DIR, "cert_test_public.pem"), "rb") as fp: 29 | encrypt_cert = asymmetric.load_certificate(fp.read()) 30 | enc_alg_list = "rc4_128_cbc".split("_") 31 | cipher, key_length, _ = enc_alg_list[0], enc_alg_list[1], enc_alg_list[2] 32 | key = util.rand_bytes(int(key_length) // 8) 33 | algorithm_id = "1.2.840.113549.3.4" 34 | encrypted_content = symmetric.rc4_encrypt(key, b"data") 35 | enc_alg_asn1 = algos.EncryptionAlgorithm( 36 | { 37 | "algorithm": algorithm_id, 38 | } 39 | ) 40 | encrypted_key = asymmetric.rsa_oaep_encrypt(encrypt_cert, key) 41 | return crypto_cms.ContentInfo( 42 | { 43 | "content_type": crypto_cms.ContentType("enveloped_data"), 44 | "content": crypto_cms.EnvelopedData( 45 | { 46 | "version": crypto_cms.CMSVersion("v0"), 47 | "recipient_infos": [ 48 | crypto_cms.KeyTransRecipientInfo( 49 | { 50 | "version": crypto_cms.CMSVersion("v0"), 51 | "rid": crypto_cms.RecipientIdentifier( 52 | { 53 | "issuer_and_serial_number": crypto_cms.IssuerAndSerialNumber( 54 | { 55 | "issuer": encrypt_cert.asn1[ 56 | "tbs_certificate" 57 | ]["issuer"], 58 | "serial_number": encrypt_cert.asn1[ 59 | "tbs_certificate" 60 | ]["serial_number"], 61 | } 62 | ) 63 | } 64 | ), 65 | "key_encryption_algorithm": crypto_cms.KeyEncryptionAlgorithm( 66 | { 67 | "algorithm": crypto_cms.KeyEncryptionAlgorithmId( 68 | "aes128_wrap" 69 | ) 70 | } 71 | ), 72 | "encrypted_key": crypto_cms.OctetString(encrypted_key), 73 | } 74 | ) 75 | ], 76 | "encrypted_content_info": crypto_cms.EncryptedContentInfo( 77 | { 78 | "content_type": crypto_cms.ContentType("data"), 79 | "content_encryption_algorithm": enc_alg_asn1, 80 | "encrypted_content": encrypted_content, 81 | } 82 | ), 83 | } 84 | ), 85 | } 86 | ).dump() 87 | 88 | 89 | def test_compress(): 90 | """Test the compression and decompression functions.""" 91 | compressed_data = cms.compress_message(b"data") 92 | assert cms.decompress_message(compressed_data) == b"data" 93 | 94 | with pytest.raises(DecompressionError): 95 | cms.decompress_message(INVALID_DATA) 96 | 97 | 98 | def test_signing(): 99 | """Test the signing and verification functions.""" 100 | # Load the signature key 101 | with open(os.path.join(TEST_DIR, "cert_test.p12"), "rb") as fp: 102 | sign_key = Organization.load_key(fp.read(), "test") 103 | with open(os.path.join(TEST_DIR, "cert_test_public.pem"), "rb") as fp: 104 | verify_cert = asymmetric.load_certificate(fp.read()) 105 | 106 | # Test failure of signature verification 107 | with pytest.raises(IntegrityError): 108 | cms.verify_message(b"data", INVALID_DATA, None) 109 | 110 | # Test signature without signed attributes 111 | cms.sign_message( 112 | b"data", digest_alg="sha256", sign_key=sign_key, use_signed_attributes=False 113 | ) 114 | 115 | # Test pss signature and verification 116 | signature = cms.sign_message( 117 | b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pss" 118 | ) 119 | cms.verify_message(b"data", signature, verify_cert) 120 | 121 | # Test unsupported signature alg 122 | with pytest.raises(AS2Exception): 123 | cms.sign_message( 124 | b"data", digest_alg="sha256", sign_key=sign_key, sign_alg="rsassa_pssa" 125 | ) 126 | 127 | # Test unsupported digest alg 128 | with pytest.raises(AS2Exception): 129 | cms.sign_message( 130 | b"data", 131 | digest_alg="sha-256", 132 | sign_key=sign_key, 133 | use_signed_attributes=False, 134 | ) 135 | 136 | 137 | def test_encryption(): 138 | """Test the encryption and decryption functions.""" 139 | with open(os.path.join(TEST_DIR, "cert_test.p12"), "rb") as fp: 140 | decrypt_key = Organization.load_key(fp.read(), "test") 141 | with open(os.path.join(TEST_DIR, "cert_test_public.pem"), "rb") as fp: 142 | encrypt_cert = asymmetric.load_certificate(fp.read()) 143 | 144 | with pytest.raises(DecryptionError): 145 | cms.decrypt_message(INVALID_DATA, None) 146 | 147 | # Test all the encryption algorithms 148 | enc_algorithms = [ 149 | "rc2_128_cbc", 150 | "rc4_128_cbc", 151 | "aes_128_cbc", 152 | "aes_192_cbc", 153 | "aes_256_cbc", 154 | "tripledes_192_cbc", 155 | ] 156 | 157 | key_enc_algos = [ 158 | "rsaes_oaep", 159 | "rsaes_pkcs1v15", 160 | ] 161 | 162 | encryption_algos = [ 163 | (alg, key_algo) for alg in enc_algorithms for key_algo in key_enc_algos 164 | ] 165 | 166 | for enc_algorithm, encryption_scheme in encryption_algos: 167 | encrypted_data = cms.encrypt_message( 168 | b"data", enc_algorithm, encrypt_cert, encryption_scheme 169 | ) 170 | _, decrypted_data = cms.decrypt_message(encrypted_data, decrypt_key) 171 | assert decrypted_data == b"data" 172 | 173 | # Test no encryption algorithm 174 | with pytest.raises(AS2Exception): 175 | cms.encrypt_message(b"data", "rc5_128_cbc", encrypt_cert) 176 | 177 | # Test no encryption algorithm on decrypt 178 | encrypted_data = cms.encrypt_message(b"data", "des_64_cbc", encrypt_cert) 179 | with pytest.raises(AS2Exception): 180 | cms.decrypt_message(encrypted_data, decrypt_key) 181 | 182 | # Test faulty key encryption algorithm 183 | with pytest.raises( 184 | AS2Exception, match="Unsupported Key Encryption Scheme: des_64_cbc" 185 | ): 186 | cms.encrypt_message(b"data", "rc2_128_cbc", encrypt_cert, "des_64_cbc") 187 | 188 | # Test unsupported key encryption algorithm 189 | encrypted_data = _encrypted_data_with_faulty_key_algo() 190 | with pytest.raises( 191 | AS2Exception, 192 | match="Failed to decrypt the payload: Could not extract decryption key.", 193 | ): 194 | cms.decrypt_message(encrypted_data, decrypt_key) 195 | -------------------------------------------------------------------------------- /pyas2lib/utils.py: -------------------------------------------------------------------------------- 1 | """Define utility functions used by the pyas2-lib package.""" 2 | 3 | import email 4 | import random 5 | import re 6 | import sys 7 | from datetime import datetime, timezone 8 | from email import message 9 | from email import policy 10 | from email.generator import BytesGenerator 11 | from io import BytesIO 12 | 13 | from OpenSSL import crypto 14 | from asn1crypto import pem 15 | 16 | from pyas2lib.exceptions import AS2Exception 17 | 18 | 19 | def unquote_as2name(quoted_name: str): 20 | """ 21 | Function converts as2 name from quoted to unquoted format. 22 | 23 | :param quoted_name: the as2 name in quoted format 24 | :return: the as2 name in unquoted format 25 | """ 26 | return email.utils.unquote(quoted_name) 27 | 28 | 29 | def quote_as2name(unquoted_name: str): 30 | """ 31 | Function converts as2 name from unquoted to quoted format. 32 | 33 | :param unquoted_name: the as2 name in unquoted format 34 | :return: the as2 name in unquoted format 35 | """ 36 | 37 | if re.search(r'[\\" ]', unquoted_name, re.M): 38 | return '"' + email.utils.quote(unquoted_name) + '"' 39 | return unquoted_name 40 | 41 | 42 | class BinaryBytesGenerator(BytesGenerator): 43 | """Override the bytes generator to better handle binary data.""" 44 | 45 | def _handle_text(self, msg): 46 | """ 47 | Handle writing the binary messages to prevent default behaviour of 48 | newline replacements. 49 | """ 50 | if ( 51 | msg.get_content_type() == "application/octet-stream" 52 | or msg.get("Content-Transfer-Encoding") == "binary" 53 | ): 54 | payload = msg.get_payload(decode=True) 55 | if payload is None: 56 | return 57 | self._fp.write(payload) 58 | else: 59 | super()._handle_text(msg) 60 | 61 | _writeBody = _handle_text 62 | 63 | 64 | def mime_to_bytes(msg: message.Message, email_policy: policy.Policy = policy.HTTP): 65 | """ 66 | Function to convert and email Message to flat string format. 67 | 68 | :param msg: email.Message to be converted to string 69 | :param email_policy: the policy to be used for flattening the message. 70 | :return: the byte string representation of the email message 71 | """ 72 | fp = BytesIO() 73 | g = BinaryBytesGenerator(fp, policy=email_policy) 74 | g.flatten(msg) 75 | return fp.getvalue() 76 | 77 | 78 | def canonicalize(email_message: message.Message, canonicalize_as_binary: bool = False): 79 | """ 80 | Function to convert an email Message to standard format string/ 81 | 82 | :param email_message: email.message.Message to be converted to standard string 83 | :param canonicalize_as_binary: force binary canonicalization 84 | :return: the standard representation of the email message in bytes 85 | """ 86 | 87 | if ( 88 | email_message.get("Content-Transfer-Encoding") == "binary" 89 | or canonicalize_as_binary 90 | ): 91 | message_header = "" 92 | message_body = email_message.get_payload(decode=True) 93 | for k, v in email_message.items(): 94 | message_header += "{}: {}\r\n".format(k, v) 95 | message_header += "\r\n" 96 | return message_header.encode("utf-8") + message_body 97 | 98 | return mime_to_bytes(email_message) 99 | 100 | 101 | def make_mime_boundary(text: str = None): 102 | """ 103 | Craft a random boundary. If text is given, ensure that the chosen 104 | boundary doesn't appear in the text. 105 | """ 106 | 107 | width = len(repr(sys.maxsize - 1)) 108 | fmt = "%%0%dd" % width 109 | 110 | token = random.randrange(sys.maxsize) 111 | boundary = ("=" * 15) + (fmt % token) + "==" 112 | if text is None: 113 | return boundary 114 | b = boundary 115 | counter = 0 116 | while True: 117 | cre = re.compile("^--" + re.escape(b) + "(--)?$", re.MULTILINE) 118 | if not cre.search(text): 119 | break 120 | b = boundary + "." + str(counter) 121 | counter += 1 122 | return b 123 | 124 | 125 | def extract_first_part(message_content: bytes, boundary: bytes): 126 | """Extract the first part of a multipart message.""" 127 | first_message = message_content.split(boundary)[1].lstrip() 128 | if first_message.endswith(b"\r\n"): 129 | first_message = first_message[:-2] 130 | else: 131 | first_message = first_message[:-1] 132 | return first_message 133 | 134 | 135 | def pem_to_der(cert: bytes, return_multiple: bool = True): 136 | """Convert a given certificate or list to PEM format.""" 137 | # initialize the certificate array 138 | cert_list = [] 139 | 140 | # If certificate is in DER then un-armour it 141 | if pem.detect(cert): 142 | for _, _, der_bytes in pem.unarmor(cert, multiple=True): 143 | cert_list.append(der_bytes) 144 | else: 145 | cert_list.append(cert) 146 | 147 | # return multiple if return_multiple is set else first element 148 | if return_multiple: 149 | return cert_list 150 | return cert_list.pop() 151 | 152 | 153 | def split_pem(pem_bytes: bytes): 154 | """ 155 | Split a give PEM file with multiple certificates. 156 | 157 | :param pem_bytes: The pem data in bytes with multiple certs 158 | :return: yields a list of certificates contained in the pem file 159 | """ 160 | started, pem_data = False, b"" 161 | for line in pem_bytes.splitlines(False): 162 | 163 | if line == b"" and not started: 164 | continue 165 | 166 | if line[0:5] in (b"-----", b"---- "): 167 | if not started: 168 | started = True 169 | else: 170 | pem_data = pem_data + line + b"\r\n" 171 | yield pem_data 172 | 173 | started = False 174 | pem_data = b"" 175 | 176 | if started: 177 | pem_data = pem_data + line + b"\r\n" 178 | 179 | 180 | def verify_certificate_chain(cert_bytes, trusted_certs, ignore_self_signed=True): 181 | """Verify a given certificate against a trust store.""" 182 | 183 | # Load the certificate 184 | certificate = crypto.load_certificate(crypto.FILETYPE_ASN1, cert_bytes) 185 | 186 | # Create a certificate store and add your trusted certs 187 | try: 188 | store = crypto.X509Store() 189 | 190 | if ignore_self_signed: 191 | store.add_cert(certificate) 192 | 193 | # Assuming the certificates are in PEM format in a trusted_certs list 194 | for _cert in trusted_certs: 195 | store.add_cert(crypto.load_certificate(crypto.FILETYPE_ASN1, _cert)) 196 | 197 | # Create a certificate context using the store and the certificate 198 | store_ctx = crypto.X509StoreContext(store, certificate) 199 | 200 | # Verify the certificate, returns None if certificate is not valid 201 | store_ctx.verify_certificate() 202 | 203 | return True 204 | 205 | except crypto.X509StoreContextError as e: 206 | raise AS2Exception( 207 | "Partner Certificate Invalid: %s" % e.args[-1][-1], "invalid-certificate" 208 | ) from e 209 | 210 | 211 | def extract_certificate_info(cert: bytes): 212 | """ 213 | Extract validity information from the certificate and return a dictionary. 214 | 215 | Provide either key with certificate (private) or public certificate. 216 | 217 | :param cert: the certificate as byte string in PEM or DER format 218 | :return: a dictionary holding certificate information: 219 | valid_from (datetime) - UTC 220 | valid_to (datetime) - UTC 221 | subject (list of name, value tuples) 222 | issuer (list of name, value tuples) 223 | serial (int) 224 | """ 225 | # initialize the cert_info dictionary 226 | cert_info = { 227 | "valid_from": None, 228 | "valid_to": None, 229 | "subject": None, 230 | "issuer": None, 231 | "serial": None, 232 | } 233 | 234 | # get certificate to DER list 235 | der = pem_to_der(cert) 236 | 237 | # iterate through the list to find the certificate 238 | for _item in der: 239 | try: 240 | # load the certificate. if element is key, exception is triggered 241 | # and next element is tried 242 | certificate = crypto.load_certificate(crypto.FILETYPE_ASN1, _item) 243 | 244 | # on successful load, extract the various fields into the dictionary 245 | cert_info["valid_from"] = datetime.strptime( 246 | certificate.get_notBefore().decode("utf8"), "%Y%m%d%H%M%SZ" 247 | ).replace(tzinfo=timezone.utc) 248 | cert_info["valid_to"] = datetime.strptime( 249 | certificate.get_notAfter().decode("utf8"), "%Y%m%d%H%M%SZ" 250 | ).replace(tzinfo=timezone.utc) 251 | cert_info["subject"] = [ 252 | tuple(item.decode("utf8", "backslashreplace") for item in sets) 253 | for sets in certificate.get_subject().get_components() 254 | ] 255 | cert_info["issuer"] = [ 256 | tuple(item.decode("utf8", "backslashreplace") for item in sets) 257 | for sets in certificate.get_issuer().get_components() 258 | ] 259 | cert_info["serial"] = certificate.get_serial_number() 260 | break 261 | except crypto.Error: 262 | continue 263 | 264 | # return the dictionary 265 | return cert_info 266 | 267 | 268 | def normalize_digest_alg(digest_alg): 269 | """Normalizes digest algorithm to lower case as some systems send it upper case""" 270 | 271 | if not isinstance(digest_alg, str): 272 | return digest_alg 273 | 274 | return digest_alg.lower() 275 | -------------------------------------------------------------------------------- /pyas2lib/tests/test_basic.py: -------------------------------------------------------------------------------- 1 | """Module for testing the basic features of pyas2.""" 2 | import pytest 3 | import socket 4 | from pyas2lib import as2 5 | from . import Pyas2TestCase 6 | 7 | 8 | class TestBasic(Pyas2TestCase): 9 | def setUp(self): 10 | self.org = as2.Organization( 11 | as2_name="some_organization", 12 | sign_key=self.private_key, 13 | sign_key_pass="test", 14 | decrypt_key=self.private_key, 15 | decrypt_key_pass="test", 16 | ) 17 | self.partner = as2.Partner( 18 | as2_name="some_partner", 19 | verify_cert=self.public_key, 20 | encrypt_cert=self.public_key, 21 | ) 22 | 23 | def test_plain_message(self): 24 | """Test Unencrypted Unsigned Uncompressed Message""" 25 | 26 | # Build an As2 message to be transmitted to partner 27 | out_message = as2.Message(self.org, self.partner) 28 | out_message.build(self.test_data) 29 | raw_out_message = out_message.headers_str + b"\r\n" + out_message.content 30 | 31 | # Parse the generated AS2 message as the partner 32 | in_message = as2.Message() 33 | status, _, _ = in_message.parse( 34 | raw_out_message, 35 | find_org_cb=self.find_org, 36 | find_partner_cb=self.find_partner, 37 | ) 38 | 39 | # Compare contents of the input and output messages 40 | self.assertEqual(status, "processed") 41 | self.assertEqual(self.test_data, in_message.content) 42 | 43 | def test_compressed_message(self): 44 | """Test Unencrypted Unsigned Compressed Message""" 45 | 46 | # Build an As2 message to be transmitted to partner 47 | self.partner.compress = True 48 | out_message = as2.Message(self.org, self.partner) 49 | out_message.build(self.test_data) 50 | raw_out_message = out_message.headers_str + b"\r\n" + out_message.content 51 | 52 | # Parse the generated AS2 message as the partner 53 | in_message = as2.Message() 54 | status, _, _ = in_message.parse( 55 | raw_out_message, 56 | find_org_cb=self.find_org, 57 | find_partner_cb=self.find_partner, 58 | ) 59 | 60 | # Compare the mic contents of the input and output messages 61 | self.assertEqual(status, "processed") 62 | self.assertTrue(in_message.compressed) 63 | self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) 64 | 65 | def test_encrypted_message(self): 66 | """Test Encrypted Unsigned Uncompressed Message""" 67 | 68 | # Build an As2 message to be transmitted to partner 69 | self.partner.encrypt = True 70 | out_message = as2.Message(self.org, self.partner) 71 | out_message.build(self.test_data) 72 | raw_out_message = out_message.headers_str + b"\r\n" + out_message.content 73 | 74 | # Parse the generated AS2 message as the partner 75 | in_message = as2.Message() 76 | status, _, _ = in_message.parse( 77 | raw_out_message, 78 | find_org_cb=self.find_org, 79 | find_partner_cb=self.find_partner, 80 | ) 81 | 82 | # Compare the mic contents of the input and output messages 83 | self.assertEqual(status, "processed") 84 | self.assertTrue(in_message.encrypted) 85 | self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) 86 | 87 | def test_signed_message(self): 88 | """Test Unencrypted Signed Uncompressed Message""" 89 | 90 | # Build an As2 message to be transmitted to partner 91 | self.partner.sign = True 92 | out_message = as2.Message(self.org, self.partner) 93 | out_message.build(self.test_data) 94 | raw_out_message = out_message.headers_str + b"\r\n" + out_message.content 95 | 96 | # Parse the generated AS2 message as the partner 97 | in_message = as2.Message() 98 | status, _, _ = in_message.parse( 99 | raw_out_message, 100 | find_org_cb=self.find_org, 101 | find_partner_cb=self.find_partner, 102 | ) 103 | 104 | # Compare the mic contents of the input and output messages 105 | self.assertEqual(status, "processed") 106 | self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) 107 | self.assertTrue(in_message.signed) 108 | self.assertEqual(out_message.mic, in_message.mic) 109 | 110 | def test_encrypted_signed_message(self): 111 | """Test Encrypted Signed Uncompressed Message""" 112 | 113 | # Build an As2 message to be transmitted to partner 114 | self.partner.sign = True 115 | self.partner.encrypt = True 116 | out_message = as2.Message(self.org, self.partner) 117 | out_message.build(self.test_data) 118 | raw_out_message = out_message.headers_str + b"\r\n" + out_message.content 119 | 120 | # Parse the generated AS2 message as the partner 121 | in_message = as2.Message() 122 | status, _, _ = in_message.parse( 123 | raw_out_message, 124 | find_org_cb=self.find_org, 125 | find_partner_cb=self.find_partner, 126 | ) 127 | 128 | # Compare the mic contents of the input and output messages 129 | self.assertEqual(status, "processed") 130 | self.assertTrue(in_message.signed) 131 | self.assertTrue(in_message.encrypted) 132 | self.assertEqual(out_message.mic, in_message.mic) 133 | self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) 134 | 135 | def test_encrypted_signed_message_dos(self): 136 | """Test Encrypted Signed Uncompressed Message with DOS line endings.""" 137 | 138 | # Build an As2 message to be transmitted to partner 139 | self.partner.sign = True 140 | self.partner.encrypt = True 141 | out_message = as2.Message(self.org, self.partner) 142 | out_message.build(self.test_data_dos) 143 | raw_out_message = out_message.headers_str + b"\r\n" + out_message.content 144 | 145 | # Parse the generated AS2 message as the partner 146 | in_message = as2.Message() 147 | status, _, _ = in_message.parse( 148 | raw_out_message, 149 | find_org_cb=self.find_org, 150 | find_partner_cb=self.find_partner, 151 | ) 152 | 153 | # Compare the mic contents of the input and output messages 154 | self.assertEqual(status, "processed") 155 | self.assertTrue(in_message.signed) 156 | self.assertTrue(in_message.encrypted) 157 | self.assertEqual(out_message.mic, in_message.mic) 158 | self.assertEqual(self.test_data_dos, in_message.content) 159 | 160 | def test_encrypted_signed_compressed_message(self): 161 | """Test Encrypted Signed Compressed Message""" 162 | 163 | # Build an As2 message to be transmitted to partner 164 | self.partner.sign = True 165 | self.partner.encrypt = True 166 | self.partner.compress = True 167 | out_message = as2.Message(self.org, self.partner) 168 | out_message.build(self.test_data) 169 | raw_out_message = out_message.headers_str + b"\r\n" + out_message.content 170 | 171 | # Parse the generated AS2 message as the partner 172 | in_message = as2.Message() 173 | status, _, _ = in_message.parse( 174 | raw_out_message, 175 | find_org_cb=self.find_org, 176 | find_partner_cb=self.find_partner, 177 | ) 178 | 179 | # Compare the mic contents of the input and output messages 180 | self.assertEqual(status, "processed") 181 | self.assertTrue(in_message.signed) 182 | self.assertTrue(in_message.encrypted) 183 | self.assertTrue(in_message.compressed) 184 | self.assertEqual(out_message.mic, in_message.mic) 185 | self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) 186 | 187 | def test_encrypted_signed_message_partnership(self): 188 | """Test Encrypted Signed Uncompressed Message with Partnership""" 189 | 190 | # Build an As2 message to be transmitted to partner 191 | self.partner.sign = True 192 | self.partner.encrypt = True 193 | out_message = as2.Message(self.org, self.partner) 194 | out_message.build(self.test_data) 195 | raw_out_message = out_message.headers_str + b"\r\n" + out_message.content 196 | 197 | # Parse the generated AS2 message as the partner 198 | in_message = as2.Message() 199 | status, _, _ = in_message.parse( 200 | raw_out_message, 201 | find_org_partner_cb=self.find_org_partner, 202 | ) 203 | 204 | # Compare the mic contents of the input and output messages 205 | self.assertEqual(status, "processed") 206 | self.assertTrue(in_message.signed) 207 | self.assertTrue(in_message.encrypted) 208 | self.assertEqual(out_message.mic, in_message.mic) 209 | self.assertEqual(self.test_data.splitlines(), in_message.content.splitlines()) 210 | 211 | def test_plain_message_with_domain(self): 212 | """Test Message building with an org domain""" 213 | 214 | # Build an As2 message to be transmitted to partner 215 | self.org.domain = "example.com" 216 | out_message = as2.Message(self.org, self.partner) 217 | out_message.build(self.test_data) 218 | self.assertEqual(out_message.message_id.split("@")[1], self.org.domain) 219 | 220 | def test_plain_message_without_domain(self): 221 | """Test Message building without an org domain""" 222 | 223 | # Build an As2 message to be transmitted to partner 224 | out_message = as2.Message(self.org, self.partner) 225 | out_message.build(self.test_data) 226 | self.assertEqual(out_message.message_id.split("@")[1], socket.getfqdn()) 227 | 228 | def test_plain_message_with_custom_message_id(self): 229 | """Test Message building with a custom message id""" 230 | 231 | # Build an As2 message to be transmitted to partner 232 | self.org.domain = "example.com" 233 | out_message = as2.Message(self.org, self.partner) 234 | out_message.build(self.test_data, message_id="some_custom_id") 235 | self.assertEqual(out_message.message_id, "some_custom_id@example.com") 236 | 237 | def test_invalid_message_id_length_raises_error(self): 238 | """Test Message building with a custom message id that's invalid""" 239 | 240 | # Build an As2 message to be transmitted to partner 241 | self.org.domain = "example.com" 242 | out_message = as2.Message(self.org, self.partner) 243 | very_long_message_id = "a" * 1000 244 | with pytest.raises(ValueError) as excinfo: 245 | out_message.build(self.test_data, message_id=very_long_message_id) 246 | assert ( 247 | "Message ID must be no more than 255 characters for compatibility" 248 | in str(excinfo.value) 249 | ) 250 | 251 | def test_invalid_cb_function_passed(self): 252 | """Checking allowed combination of CB functions""" 253 | 254 | # Create AS2 message and parse with wrong combination of callback functions 255 | 256 | as2_message = as2.Message() 257 | with pytest.raises( 258 | TypeError, 259 | match="Incorrect arguments passed: either find_org_cb and find_partner_cb " 260 | "or only find_org_partner_cb must be passed.", 261 | ): 262 | 263 | _, _, _ = as2_message.parse( 264 | "abc", 265 | find_org_partner_cb=self.find_org_partner, 266 | find_partner_cb=self.find_partner, 267 | ) 268 | 269 | with pytest.raises( 270 | TypeError, 271 | match="Incorrect arguments passed: either find_org_cb and find_partner_cb " 272 | "or only find_org_partner_cb must be passed.", 273 | ): 274 | _, _, _ = as2_message.parse( 275 | "abc", 276 | find_org_partner_cb=self.find_org_partner, 277 | find_org_cb=self.find_org, 278 | ) 279 | 280 | with pytest.raises( 281 | TypeError, 282 | match="Incorrect arguments passed: either find_org_cb and find_partner_cb " 283 | "or only find_org_partner_cb must be passed.", 284 | ): 285 | _, _, _ = as2_message.parse( 286 | "abc", 287 | find_org_partner_cb=self.find_org_partner, 288 | find_org_cb=self.find_org, 289 | find_partner_cb=self.find_partner, 290 | ) 291 | 292 | def find_org(self, as2_id): 293 | return self.org 294 | 295 | def find_partner(self, as2_id): 296 | return self.partner 297 | 298 | def find_org_partner(self, as2_org, as2_partner): 299 | return self.org, self.partner 300 | -------------------------------------------------------------------------------- /pyas2lib/cms.py: -------------------------------------------------------------------------------- 1 | """Define functions related to the CMS operations such as encrypting, signature, etc.""" 2 | import hashlib 3 | import zlib 4 | from datetime import datetime, timezone 5 | 6 | from asn1crypto import cms, core, algos 7 | from asn1crypto.cms import SMIMECapabilityIdentifier 8 | from oscrypto import asymmetric, symmetric, util 9 | 10 | from pyas2lib.constants import DIGEST_ALGORITHMS 11 | from pyas2lib.exceptions import ( 12 | AS2Exception, 13 | DecompressionError, 14 | DecryptionError, 15 | IntegrityError, 16 | ) 17 | from pyas2lib.utils import normalize_digest_alg 18 | 19 | 20 | def compress_message(data_to_compress): 21 | """Function compresses data and returns the generated ASN.1 22 | 23 | :param data_to_compress: A byte string of the data to be compressed 24 | 25 | :return: A CMS ASN.1 byte string of the compressed data. 26 | """ 27 | compressed_content = cms.ParsableOctetString(zlib.compress(data_to_compress)) 28 | return cms.ContentInfo( 29 | { 30 | "content_type": cms.ContentType("compressed_data"), 31 | "content": cms.CompressedData( 32 | { 33 | "version": cms.CMSVersion("v0"), 34 | "compression_algorithm": cms.CompressionAlgorithm( 35 | {"algorithm": cms.CompressionAlgorithmId("zlib")} 36 | ), 37 | "encap_content_info": cms.EncapsulatedContentInfo( 38 | { 39 | "content_type": cms.ContentType("data"), 40 | "content": compressed_content, 41 | } 42 | ), 43 | } 44 | ), 45 | } 46 | ).dump() 47 | 48 | 49 | def decompress_message(compressed_data): 50 | """Function parses an ASN.1 compressed message and extracts/decompresses 51 | the original message. 52 | 53 | :param compressed_data: A CMS ASN.1 byte string containing the compressed 54 | data. 55 | 56 | :return: A byte string containing the decompressed original message. 57 | """ 58 | try: 59 | cms_content = cms.ContentInfo.load(compressed_data) 60 | if cms_content["content_type"].native == "compressed_data": 61 | return cms_content["content"].decompressed 62 | raise DecompressionError("Compressed data not found in ASN.1 ") 63 | 64 | except Exception as e: 65 | raise DecompressionError("Decompression failed with cause: {}".format(e)) from e 66 | 67 | 68 | def encrypt_message( 69 | data_to_encrypt, enc_alg, encryption_cert, key_enc_alg="rsaes_pkcs1v15" 70 | ): 71 | """Function encrypts data and returns the generated ASN.1 72 | 73 | :param data_to_encrypt: A byte string of the data to be encrypted 74 | :param enc_alg: The algorithm to be used for encrypting the data 75 | :param encryption_cert: The certificate to be used for encrypting the data 76 | :param key_enc_alg: The algo for the key encryption: rsaes_pkcs1v15 (default) or rsaes_oaep 77 | 78 | :return: A CMS ASN.1 byte string of the encrypted data. 79 | """ 80 | 81 | enc_alg_list = enc_alg.split("_") 82 | cipher, key_length, _ = enc_alg_list[0], enc_alg_list[1], enc_alg_list[2] 83 | 84 | # Generate the symmetric encryption key and encrypt the message 85 | key = util.rand_bytes(int(key_length) // 8) 86 | if cipher == "tripledes": 87 | algorithm_id = "1.2.840.113549.3.7" 88 | iv, encrypted_content = symmetric.tripledes_cbc_pkcs5_encrypt( 89 | key, data_to_encrypt, None 90 | ) 91 | enc_alg_asn1 = algos.EncryptionAlgorithm( 92 | {"algorithm": algorithm_id, "parameters": cms.OctetString(iv)} 93 | ) 94 | 95 | elif cipher == "rc2": 96 | algorithm_id = "1.2.840.113549.3.2" 97 | iv, encrypted_content = symmetric.rc2_cbc_pkcs5_encrypt( 98 | key, data_to_encrypt, None 99 | ) 100 | enc_alg_asn1 = algos.EncryptionAlgorithm( 101 | { 102 | "algorithm": algorithm_id, 103 | "parameters": algos.Rc2Params({"iv": cms.OctetString(iv)}), 104 | } 105 | ) 106 | 107 | elif cipher == "rc4": 108 | algorithm_id = "1.2.840.113549.3.4" 109 | encrypted_content = symmetric.rc4_encrypt(key, data_to_encrypt) 110 | enc_alg_asn1 = algos.EncryptionAlgorithm( 111 | { 112 | "algorithm": algorithm_id, 113 | } 114 | ) 115 | 116 | elif cipher == "aes": 117 | if key_length == "128": 118 | algorithm_id = "2.16.840.1.101.3.4.1.2" 119 | elif key_length == "192": 120 | algorithm_id = "2.16.840.1.101.3.4.1.22" 121 | else: 122 | algorithm_id = "2.16.840.1.101.3.4.1.42" 123 | 124 | iv, encrypted_content = symmetric.aes_cbc_pkcs7_encrypt( 125 | key, data_to_encrypt, None 126 | ) 127 | enc_alg_asn1 = algos.EncryptionAlgorithm( 128 | {"algorithm": algorithm_id, "parameters": cms.OctetString(iv)} 129 | ) 130 | elif cipher == "des": 131 | algorithm_id = "1.3.14.3.2.7" 132 | iv, encrypted_content = symmetric.des_cbc_pkcs5_encrypt( 133 | key, data_to_encrypt, None 134 | ) 135 | enc_alg_asn1 = algos.EncryptionAlgorithm( 136 | {"algorithm": algorithm_id, "parameters": cms.OctetString(iv)} 137 | ) 138 | else: 139 | raise AS2Exception("Unsupported Encryption Algorithm") 140 | 141 | # Encrypt the key and build the ASN.1 message 142 | if key_enc_alg == "rsaes_pkcs1v15": 143 | encrypted_key = asymmetric.rsa_pkcs1v15_encrypt(encryption_cert, key) 144 | elif key_enc_alg == "rsaes_oaep": 145 | encrypted_key = asymmetric.rsa_oaep_encrypt(encryption_cert, key) 146 | else: 147 | raise AS2Exception(f"Unsupported Key Encryption Scheme: {key_enc_alg}") 148 | 149 | return cms.ContentInfo( 150 | { 151 | "content_type": cms.ContentType("enveloped_data"), 152 | "content": cms.EnvelopedData( 153 | { 154 | "version": cms.CMSVersion("v0"), 155 | "recipient_infos": [ 156 | cms.KeyTransRecipientInfo( 157 | { 158 | "version": cms.CMSVersion("v0"), 159 | "rid": cms.RecipientIdentifier( 160 | { 161 | "issuer_and_serial_number": cms.IssuerAndSerialNumber( 162 | { 163 | "issuer": encryption_cert.asn1[ 164 | "tbs_certificate" 165 | ]["issuer"], 166 | "serial_number": encryption_cert.asn1[ 167 | "tbs_certificate" 168 | ]["serial_number"], 169 | } 170 | ) 171 | } 172 | ), 173 | "key_encryption_algorithm": cms.KeyEncryptionAlgorithm( 174 | { 175 | "algorithm": cms.KeyEncryptionAlgorithmId( 176 | key_enc_alg 177 | ) 178 | } 179 | ), 180 | "encrypted_key": cms.OctetString(encrypted_key), 181 | } 182 | ) 183 | ], 184 | "encrypted_content_info": cms.EncryptedContentInfo( 185 | { 186 | "content_type": cms.ContentType("data"), 187 | "content_encryption_algorithm": enc_alg_asn1, 188 | "encrypted_content": encrypted_content, 189 | } 190 | ), 191 | } 192 | ), 193 | } 194 | ).dump() 195 | 196 | 197 | def decrypt_message(encrypted_data, decryption_key): 198 | """Function parses an ASN.1 encrypted message and extracts/decrypts the original message. 199 | 200 | :param encrypted_data: A CMS ASN.1 byte string containing the encrypted data. 201 | :param decryption_key: The key to be used for decrypting the data. 202 | 203 | :return: A byte string containing the decrypted original message. 204 | """ 205 | 206 | cms_content = cms.ContentInfo.load(encrypted_data) 207 | cipher, decrypted_content = None, None 208 | 209 | if cms_content["content_type"].native == "enveloped_data": 210 | recipient_info = cms_content["content"]["recipient_infos"][0].parse() 211 | key_enc_alg = recipient_info["key_encryption_algorithm"]["algorithm"].native 212 | encrypted_key = recipient_info["encrypted_key"].native 213 | 214 | try: 215 | if cms.KeyEncryptionAlgorithmId( 216 | key_enc_alg 217 | ) == cms.KeyEncryptionAlgorithmId("rsaes_pkcs1v15"): 218 | key = asymmetric.rsa_pkcs1v15_decrypt(decryption_key[0], encrypted_key) 219 | 220 | elif cms.KeyEncryptionAlgorithmId( 221 | key_enc_alg 222 | ) == cms.KeyEncryptionAlgorithmId("rsaes_oaep"): 223 | key = asymmetric.rsa_oaep_decrypt(decryption_key[0], encrypted_key) 224 | else: 225 | raise AS2Exception( 226 | f"Unsupported Key Encryption Algorithm {key_enc_alg}" 227 | ) 228 | except Exception as e: 229 | raise DecryptionError( 230 | "Failed to decrypt the payload: Could not extract decryption key." 231 | ) from e 232 | 233 | alg = cms_content["content"]["encrypted_content_info"][ 234 | "content_encryption_algorithm" 235 | ] 236 | encapsulated_data = cms_content["content"]["encrypted_content_info"][ 237 | "encrypted_content" 238 | ].native 239 | 240 | try: 241 | if alg["algorithm"].native == "rc4": 242 | decrypted_content = symmetric.rc4_decrypt(key, encapsulated_data) 243 | elif alg.encryption_cipher == "tripledes": 244 | cipher = "tripledes_192_cbc" 245 | decrypted_content = symmetric.tripledes_cbc_pkcs5_decrypt( 246 | key, encapsulated_data, alg.encryption_iv 247 | ) 248 | elif alg.encryption_cipher == "aes": 249 | decrypted_content = symmetric.aes_cbc_pkcs7_decrypt( 250 | key, encapsulated_data, alg.encryption_iv 251 | ) 252 | elif alg.encryption_cipher == "rc2": 253 | decrypted_content = symmetric.rc2_cbc_pkcs5_decrypt( 254 | key, encapsulated_data, alg["parameters"]["iv"].native 255 | ) 256 | else: 257 | raise AS2Exception("Unsupported Encryption Algorithm") 258 | except Exception as e: 259 | raise DecryptionError("Failed to decrypt the payload: {}".format(e)) from e 260 | else: 261 | raise DecryptionError("Encrypted data not found in ASN.1 ") 262 | 263 | return cipher, decrypted_content 264 | 265 | 266 | def sign_message( 267 | data_to_sign, 268 | digest_alg, 269 | sign_key, 270 | sign_alg="rsassa_pkcs1v15", 271 | use_signed_attributes=True, 272 | ): 273 | """Function signs the data and returns the generated ASN.1 274 | 275 | :param data_to_sign: A byte string of the data to be signed. 276 | 277 | :param digest_alg: The digest algorithm to be used for generating the signature. 278 | 279 | :param sign_key: The key to be used for generating the signature. 280 | 281 | :param sign_alg: The algorithm to be used for signing the message. 282 | 283 | :param use_signed_attributes: Optional attribute to indicate weather the 284 | CMS signature attributes should be included in the signature or not. 285 | 286 | :return: A CMS ASN.1 byte string of the signed data. 287 | """ 288 | digest_alg = normalize_digest_alg(digest_alg) 289 | if digest_alg not in DIGEST_ALGORITHMS: 290 | raise AS2Exception("Unsupported Digest Algorithm") 291 | 292 | if use_signed_attributes: 293 | digest_func = hashlib.new(digest_alg) 294 | digest_func.update(data_to_sign) 295 | message_digest = digest_func.digest() 296 | 297 | signed_attributes = cms.CMSAttributes( 298 | [ 299 | cms.CMSAttribute( 300 | { 301 | "type": cms.CMSAttributeType("content_type"), 302 | "values": cms.SetOfContentType([cms.ContentType("data")]), 303 | } 304 | ), 305 | cms.CMSAttribute( 306 | { 307 | "type": cms.CMSAttributeType("signing_time"), 308 | "values": cms.SetOfTime( 309 | [ 310 | cms.Time( 311 | { 312 | "utc_time": core.UTCTime( 313 | datetime.utcnow().replace( 314 | tzinfo=timezone.utc 315 | ) 316 | ) 317 | } 318 | ) 319 | ] 320 | ), 321 | } 322 | ), 323 | cms.CMSAttribute( 324 | { 325 | "type": cms.CMSAttributeType("message_digest"), 326 | "values": cms.SetOfOctetString( 327 | [core.OctetString(message_digest)] 328 | ), 329 | } 330 | ), 331 | cms.CMSAttribute( 332 | { 333 | "type": cms.CMSAttributeType("1.2.840.113549.1.9.15"), 334 | "values": cms.SetOfSMIMECapabilites( 335 | [ 336 | cms.SMIMECapabilites( 337 | [ 338 | SMIMECapabilityIdentifier( 339 | {"capability_id": "2.16.840.1.101.3.4.1.42"} 340 | ), 341 | SMIMECapabilityIdentifier( 342 | {"capability_id": "2.16.840.1.101.3.4.1.2"} 343 | ), 344 | SMIMECapabilityIdentifier( 345 | {"capability_id": "1.2.840.113549.3.7"} 346 | ), 347 | SMIMECapabilityIdentifier( 348 | { 349 | "capability_id": "1.2.840.113549.3.2", 350 | "parameters": core.Integer(128), 351 | } 352 | ), 353 | SMIMECapabilityIdentifier( 354 | { 355 | "capability_id": "1.2.840.113549.3.4", 356 | "parameters": core.Integer(128), 357 | } 358 | ), 359 | ] 360 | ) 361 | ] 362 | ), 363 | } 364 | ), 365 | ] 366 | ) 367 | else: 368 | signed_attributes = None 369 | 370 | # Generate the signature 371 | data_to_sign = signed_attributes.dump() if signed_attributes else data_to_sign 372 | if sign_alg == "rsassa_pkcs1v15": 373 | signature = asymmetric.rsa_pkcs1v15_sign(sign_key[0], data_to_sign, digest_alg) 374 | elif sign_alg == "rsassa_pss": 375 | signature = asymmetric.rsa_pss_sign(sign_key[0], data_to_sign, digest_alg) 376 | else: 377 | raise AS2Exception("Unsupported Signature Algorithm") 378 | 379 | return cms.ContentInfo( 380 | { 381 | "content_type": cms.ContentType("signed_data"), 382 | "content": cms.SignedData( 383 | { 384 | "version": cms.CMSVersion("v1"), 385 | "digest_algorithms": cms.DigestAlgorithms( 386 | [ 387 | algos.DigestAlgorithm( 388 | {"algorithm": algos.DigestAlgorithmId(digest_alg)} 389 | ) 390 | ] 391 | ), 392 | "encap_content_info": cms.ContentInfo( 393 | {"content_type": cms.ContentType("data")} 394 | ), 395 | "certificates": cms.CertificateSet( 396 | [cms.CertificateChoices({"certificate": sign_key[1].asn1})] 397 | ), 398 | "signer_infos": cms.SignerInfos( 399 | [ 400 | cms.SignerInfo( 401 | { 402 | "version": cms.CMSVersion("v1"), 403 | "sid": cms.SignerIdentifier( 404 | { 405 | "issuer_and_serial_number": cms.IssuerAndSerialNumber( 406 | { 407 | "issuer": sign_key[1].asn1[ 408 | "tbs_certificate" 409 | ]["issuer"], 410 | "serial_number": sign_key[1].asn1[ 411 | "tbs_certificate" 412 | ]["serial_number"], 413 | } 414 | ) 415 | } 416 | ), 417 | "digest_algorithm": algos.DigestAlgorithm( 418 | { 419 | "algorithm": algos.DigestAlgorithmId( 420 | digest_alg 421 | ) 422 | } 423 | ), 424 | "signed_attrs": signed_attributes, 425 | "signature_algorithm": algos.SignedDigestAlgorithm( 426 | { 427 | "algorithm": algos.SignedDigestAlgorithmId( 428 | sign_alg 429 | ) 430 | } 431 | ), 432 | "signature": core.OctetString(signature), 433 | } 434 | ) 435 | ] 436 | ), 437 | } 438 | ), 439 | } 440 | ).dump() 441 | 442 | 443 | def verify_message(data_to_verify, signature, verify_cert): 444 | """Function parses an ASN.1 encrypted message and extracts/decrypts the original message. 445 | 446 | :param data_to_verify: A byte string of the data to be verified against the signature. 447 | :param signature: A CMS ASN.1 byte string containing the signature. 448 | :param verify_cert: The certificate to be used for verifying the signature. 449 | 450 | :return: The digest algorithm that was used in the signature. 451 | """ 452 | 453 | cms_content = cms.ContentInfo.load(signature) 454 | digest_alg = None 455 | if cms_content["content_type"].native == "signed_data": 456 | 457 | for signer in cms_content["content"]["signer_infos"]: 458 | 459 | digest_alg = normalize_digest_alg( 460 | signer["digest_algorithm"]["algorithm"].native 461 | ) 462 | if digest_alg not in DIGEST_ALGORITHMS: 463 | raise Exception("Unsupported Digest Algorithm") 464 | 465 | sig_alg = signer["signature_algorithm"].signature_algo 466 | sig = signer["signature"].native 467 | signed_data = data_to_verify 468 | 469 | if signer["signed_attrs"]: 470 | attr_dict = {} 471 | for attr in signer["signed_attrs"]: 472 | try: 473 | attr_dict[attr.native["type"]] = attr.native["values"] 474 | except (ValueError, KeyError): 475 | continue 476 | 477 | message_digest = bytes() 478 | for d in attr_dict["message_digest"]: 479 | message_digest += d 480 | 481 | digest_func = hashlib.new(digest_alg) 482 | digest_func.update(data_to_verify) 483 | calc_message_digest = digest_func.digest() 484 | if message_digest != calc_message_digest: 485 | raise IntegrityError( 486 | "Failed to verify message signature: Message Digest does not match." 487 | ) 488 | 489 | signed_data = signer["signed_attrs"].untag().dump() 490 | 491 | try: 492 | if sig_alg == "rsassa_pkcs1v15": 493 | asymmetric.rsa_pkcs1v15_verify( 494 | verify_cert, sig, signed_data, digest_alg 495 | ) 496 | elif sig_alg == "rsassa_pss": 497 | asymmetric.rsa_pss_verify(verify_cert, sig, signed_data, digest_alg) 498 | else: 499 | raise AS2Exception("Unsupported Signature Algorithm") 500 | except Exception as e: 501 | raise IntegrityError( 502 | "Failed to verify message signature: {}".format(e) 503 | ) from e 504 | else: 505 | raise IntegrityError("Signed data not found in ASN.1 ") 506 | 507 | return digest_alg 508 | -------------------------------------------------------------------------------- /pyas2lib/tests/test_advanced.py: -------------------------------------------------------------------------------- 1 | """Module for testing the advanced features of pyas2lib.""" 2 | import base64 3 | import os 4 | from email import message 5 | from email import message_from_bytes as parse_mime 6 | 7 | import pytest 8 | from pyas2lib import as2 9 | from pyas2lib.exceptions import ImproperlyConfigured 10 | from pyas2lib.tests import Pyas2TestCase, TEST_DIR 11 | 12 | 13 | class TestAdvanced(Pyas2TestCase): 14 | def setUp(self): 15 | self.org = as2.Organization( 16 | as2_name="some_organization", 17 | sign_key=self.private_key, 18 | sign_key_pass="test", 19 | decrypt_key=self.private_key, 20 | decrypt_key_pass="test", 21 | ) 22 | self.partner = as2.Partner( 23 | as2_name="some_partner", 24 | verify_cert=self.public_key, 25 | encrypt_cert=self.public_key, 26 | ) 27 | 28 | def test_binary_message(self): 29 | """Test Encrypted Signed Binary Message""" 30 | 31 | # Build an As2 message to be transmitted to partner 32 | self.partner.sign = True 33 | self.partner.encrypt = True 34 | self.partner.compress = True 35 | out_message = as2.Message(self.org, self.partner) 36 | test_message_path = os.path.join(TEST_DIR, "payload.binary") 37 | with open(test_message_path, "rb") as bin_file: 38 | original_message = bin_file.read() 39 | out_message.build( 40 | original_message, 41 | filename="payload.binary", 42 | content_type="application/octet-stream", 43 | ) 44 | raw_out_message = out_message.headers_str + b"\r\n" + out_message.content 45 | 46 | # Parse the generated AS2 message as the partner 47 | in_message = as2.Message() 48 | status, _, _ = in_message.parse( 49 | raw_out_message, 50 | find_org_cb=self.find_org, 51 | find_partner_cb=self.find_partner, 52 | find_message_cb=lambda x, y: False, 53 | ) 54 | 55 | # Compare the mic contents of the input and output messages 56 | self.assertEqual(status, "processed") 57 | self.assertEqual(original_message, in_message.payload.get_payload(decode=True)) 58 | self.assertTrue(in_message.signed) 59 | self.assertTrue(in_message.encrypted) 60 | self.assertEqual(out_message.mic, in_message.mic) 61 | 62 | def test_partner_not_found(self): 63 | """Test case where partner and organization is not found""" 64 | 65 | # Build an As2 message to be transmitted to partner 66 | self.partner.sign = True 67 | self.partner.encrypt = True 68 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 69 | self.out_message = as2.Message(self.org, self.partner) 70 | self.out_message.build(self.test_data) 71 | 72 | # Parse the generated AS2 message as the partner 73 | raw_out_message = ( 74 | self.out_message.headers_str + b"\r\n" + self.out_message.content 75 | ) 76 | in_message = as2.Message() 77 | _, _, mdn = in_message.parse( 78 | raw_out_message, 79 | find_org_cb=self.find_org, 80 | find_partner_cb=lambda x: None, 81 | find_message_cb=lambda x, y: False, 82 | ) 83 | 84 | out_mdn = as2.Mdn() 85 | status, detailed_status = out_mdn.parse( 86 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 87 | ) 88 | 89 | self.assertEqual(status, "processed/Error") 90 | self.assertEqual(detailed_status, "unknown-trading-partner") 91 | 92 | # Parse again but this time make without organization 93 | in_message = as2.Message() 94 | _, _, mdn = in_message.parse( 95 | raw_out_message, 96 | find_org_cb=lambda x: None, 97 | find_partner_cb=self.find_partner, 98 | find_message_cb=lambda x, y: False, 99 | ) 100 | 101 | out_mdn = as2.Mdn() 102 | status, detailed_status = out_mdn.parse( 103 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 104 | ) 105 | self.assertEqual(status, "processed/Error") 106 | self.assertEqual(detailed_status, "unknown-trading-partner") 107 | 108 | def test_duplicate_message(self): 109 | """Test case where a duplicate message is sent to the partner""" 110 | 111 | # Build an As2 message to be transmitted to partner 112 | self.partner.sign = True 113 | self.partner.encrypt = True 114 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 115 | self.out_message = as2.Message(self.org, self.partner) 116 | self.out_message.build(self.test_data) 117 | 118 | # Parse the generated AS2 message as the partner 119 | raw_out_message = ( 120 | self.out_message.headers_str + b"\r\n" + self.out_message.content 121 | ) 122 | in_message = as2.Message() 123 | _, _, mdn = in_message.parse( 124 | raw_out_message, 125 | find_org_cb=self.find_org, 126 | find_partner_cb=self.find_partner, 127 | find_message_cb=lambda x, y: True, 128 | ) 129 | 130 | out_mdn = as2.Mdn() 131 | status, detailed_status = out_mdn.parse( 132 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 133 | ) 134 | self.assertEqual(status, "processed/Warning") 135 | self.assertEqual(detailed_status, "duplicate-document") 136 | 137 | def test_failed_decompression(self): 138 | """Test case where message decompression has failed""" 139 | 140 | # Build an As2 message to be transmitted to partner 141 | self.partner.compress = True 142 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 143 | self.out_message = as2.Message(self.org, self.partner) 144 | self.out_message.build(self.test_data) 145 | 146 | # Parse the generated AS2 message as the partner 147 | raw_out_message = ( 148 | self.out_message.headers_str + b"\r\n" + base64.b64encode(b"xxxxx") 149 | ) 150 | in_message = as2.Message() 151 | _, exec_info, mdn = in_message.parse( 152 | raw_out_message, 153 | find_org_cb=self.find_org, 154 | find_partner_cb=self.find_partner, 155 | ) 156 | 157 | out_mdn = as2.Mdn() 158 | status, detailed_status = out_mdn.parse( 159 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 160 | ) 161 | self.assertEqual(status, "processed/Error") 162 | self.assertEqual(detailed_status, "decompression-failed") 163 | 164 | def test_insufficient_security(self): 165 | """Test case where message security is not as per the configuration""" 166 | 167 | # Build an As2 message to be transmitted to partner 168 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 169 | self.out_message = as2.Message(self.org, self.partner) 170 | self.out_message.build(self.test_data) 171 | 172 | # Parse the generated AS2 message as the partner 173 | self.partner.encrypt = True 174 | raw_out_message = ( 175 | self.out_message.headers_str + b"\r\n" + self.out_message.content 176 | ) 177 | in_message = as2.Message() 178 | status, (exc, _), mdn = in_message.parse( 179 | raw_out_message, 180 | find_org_cb=self.find_org, 181 | find_partner_cb=self.find_partner, 182 | find_message_cb=lambda x, y: False, 183 | ) 184 | self.assertEqual(status, "processed/Error") 185 | self.assertEqual(exc.disposition_modifier, "insufficient-message-security") 186 | 187 | # Try again for signing check 188 | self.partner.encrypt = False 189 | self.partner.sign = True 190 | in_message = as2.Message() 191 | status, (exc, _), mdn = in_message.parse( 192 | raw_out_message, 193 | find_org_cb=self.find_org, 194 | find_partner_cb=self.find_partner, 195 | find_message_cb=lambda x, y: False, 196 | ) 197 | self.assertEqual(status, "processed/Error") 198 | self.assertEqual(exc.disposition_modifier, "insufficient-message-security") 199 | 200 | def test_failed_decryption(self): 201 | """Test case where message decryption has failed""" 202 | 203 | # Build an As2 message to be transmitted to partner 204 | self.partner.encrypt = True 205 | self.partner.encrypt_cert = self.mecas2_public_key 206 | self.partner.validate_certs = False 207 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 208 | self.out_message = as2.Message(self.org, self.partner) 209 | self.out_message.build(self.test_data) 210 | 211 | # Parse the generated AS2 message as the partner 212 | raw_out_message = ( 213 | self.out_message.headers_str + b"\r\n" + self.out_message.content 214 | ) 215 | in_message = as2.Message() 216 | _, exec_info, mdn = in_message.parse( 217 | raw_out_message, 218 | find_org_cb=self.find_org, 219 | find_partner_cb=self.find_partner, 220 | find_message_cb=lambda x, y: False, 221 | ) 222 | 223 | out_mdn = as2.Mdn() 224 | status, detailed_status = out_mdn.parse( 225 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 226 | ) 227 | self.assertEqual(status, "processed/Error") 228 | self.assertEqual(detailed_status, "decryption-failed") 229 | 230 | def test_failed_signature(self): 231 | """Test case where signature verification has failed""" 232 | 233 | # Build an As2 message to be transmitted to partner 234 | self.partner.sign = True 235 | self.partner.verify_cert = self.mecas2_public_key 236 | self.partner.validate_certs = False 237 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 238 | self.out_message = as2.Message(self.org, self.partner) 239 | self.out_message.build(self.test_data) 240 | 241 | # Parse the generated AS2 message as the partner 242 | raw_out_message = ( 243 | self.out_message.headers_str + b"\r\n" + self.out_message.content 244 | ) 245 | in_message = as2.Message() 246 | _, exec_info, mdn = in_message.parse( 247 | raw_out_message, 248 | find_org_cb=self.find_org, 249 | find_partner_cb=self.find_partner, 250 | find_message_cb=lambda x, y: False, 251 | ) 252 | 253 | out_mdn = as2.Mdn() 254 | status, detailed_status = out_mdn.parse( 255 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 256 | ) 257 | self.assertEqual(status, "processed/Error") 258 | self.assertEqual(detailed_status, "authentication-failed") 259 | 260 | def test_verify_certificate(self): 261 | """Test case where we have try to load an expired cert""" 262 | 263 | # First test with a certificate with invalid root 264 | cert_path = os.path.join(TEST_DIR, "verify_cert_test1.pem") 265 | with open(cert_path, "rb") as cert_file: 266 | try: 267 | as2.Partner(as2_name="some_partner", verify_cert=cert_file.read()) 268 | except as2.AS2Exception as e: 269 | self.assertIn("unable to get local issuer certificate", str(e)) 270 | 271 | # Test with an expired certificate 272 | cert_path = os.path.join(TEST_DIR, "verify_cert_test2.cer") 273 | with open(cert_path, "rb") as cert_file: 274 | try: 275 | as2.Partner(as2_name="some_partner", verify_cert=cert_file.read()) 276 | except as2.AS2Exception as e: 277 | self.assertIn("certificate has expired", str(e)) 278 | 279 | # Test with a chain certificate 280 | cert_path = os.path.join(TEST_DIR, "verify_cert_test3.pem") 281 | with open(cert_path, "rb") as cert_file: 282 | try: 283 | as2.Partner(as2_name="some_partner", verify_cert=cert_file.read()) 284 | except as2.AS2Exception as e: 285 | self.assertIn("unable to get local issuer certificate", str(e)) 286 | 287 | # Test chain certificate with the ca 288 | cert_ca_path = os.path.join(TEST_DIR, "verify_cert_test3.ca") 289 | with open(cert_path, "rb") as cert_file: 290 | with open(cert_ca_path, "rb") as cert_ca_file: 291 | try: 292 | as2.Partner( 293 | as2_name="some_partner", 294 | verify_cert=cert_file.read(), 295 | verify_cert_ca=cert_ca_file.read(), 296 | ) 297 | except as2.AS2Exception as e: 298 | self.fail("Failed to load chain certificate: %s" % e) 299 | 300 | def test_load_private_key(self): 301 | """Test case where we have try to load keys in different formats""" 302 | 303 | # First test with a pkcs12 key file 304 | cert_path = os.path.join(TEST_DIR, "cert_test.p12") 305 | with open(cert_path, "rb") as cert_file: 306 | try: 307 | as2.Organization( 308 | as2_name="some_org", sign_key=cert_file.read(), sign_key_pass="test" 309 | ) 310 | except as2.AS2Exception as e: 311 | self.fail("Failed to load p12 private key: %s" % e) 312 | 313 | # Now test with a pem encoded key file 314 | cert_path = os.path.join(TEST_DIR, "cert_test.pem") 315 | with open(cert_path, "rb") as cert_file: 316 | try: 317 | as2.Organization( 318 | as2_name="some_org", sign_key=cert_file.read(), sign_key_pass="test" 319 | ) 320 | except as2.AS2Exception as e: 321 | self.fail("Failed to load pem private key: %s" % e) 322 | 323 | def test_partner_checks(self): 324 | """Test the checks for the partner on initialization.""" 325 | with self.assertRaises(ImproperlyConfigured): 326 | as2.Partner("a partner", digest_alg="xyz") 327 | 328 | with self.assertRaises(ImproperlyConfigured): 329 | as2.Partner("a partner", enc_alg="xyz") 330 | 331 | with self.assertRaises(ImproperlyConfigured): 332 | as2.Partner("a partner", mdn_mode="xyz") 333 | 334 | with self.assertRaises(ImproperlyConfigured): 335 | as2.Partner("a partner", mdn_digest_alg="xyz") 336 | 337 | with self.assertRaises(ImproperlyConfigured): 338 | as2.Partner("a partner", sign_alg="xyz") 339 | 340 | with self.assertRaises(ImproperlyConfigured): 341 | as2.Partner("a partner", key_enc_alg="xyz") 342 | 343 | def test_message_checks(self): 344 | """Test the checks and other features of Message.""" 345 | msg = as2.Message() 346 | assert msg.content == "" 347 | assert msg.headers == {} 348 | assert msg.headers_str == b"" 349 | 350 | msg.payload = message.Message() 351 | msg.payload.set_payload(b"data") 352 | assert msg.content == b"data" 353 | 354 | org = as2.Organization(as2_name="AS2 Server") 355 | partner = as2.Partner(as2_name="AS2 Partner", sign=True) 356 | msg = as2.Message(sender=org, receiver=partner) 357 | with self.assertRaises(ImproperlyConfigured): 358 | msg.build(b"data") 359 | 360 | msg.receiver.sign = False 361 | msg.receiver.encrypt = True 362 | with self.assertRaises(ImproperlyConfigured): 363 | msg.build(b"data") 364 | 365 | msg.receiver.encrypt = False 366 | msg.receiver.mdn_mode = "ASYNC" 367 | with self.assertRaises(ImproperlyConfigured): 368 | msg.build(b"data") 369 | 370 | msg.sender.mdn_url = "http://localhost/pyas2/as2receive" 371 | msg.build(b"data") 372 | 373 | def test_mdn_checks(self): 374 | """Test the checks and other features of MDN.""" 375 | mdn = as2.Mdn() 376 | assert mdn.content == "" 377 | assert mdn.headers == {} 378 | assert mdn.headers_str == b"" 379 | 380 | def test_mdn_not_found(self): 381 | """Test that the MDN parser raises MDN not found when a non MDN message is passed.""" 382 | self.partner.encrypt = True 383 | self.partner.validate_certs = False 384 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 385 | self.out_message = as2.Message(self.org, self.partner) 386 | self.out_message.build(self.test_data) 387 | 388 | # Parse the AS2 message as an MDN 389 | mdn = as2.Mdn() 390 | raw_out_message = ( 391 | self.out_message.headers_str + b"\r\n" + self.out_message.content 392 | ) 393 | status, detailed_status = mdn.parse( 394 | raw_out_message, find_message_cb=self.find_message 395 | ) 396 | self.assertEqual(status, "failed/Failure") 397 | self.assertEqual(detailed_status, "mdn-not-found") 398 | 399 | def test_mdn_original_message_not_found(self): 400 | """Test that the MDN parser raises MDN not found when a non MDN message is passed.""" 401 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 402 | self.out_message = as2.Message(self.org, self.partner) 403 | self.out_message.build(self.test_data) 404 | 405 | # Parse the generated AS2 message as the partner 406 | raw_out_message = ( 407 | self.out_message.headers_str + b"\r\n" + self.out_message.content 408 | ) 409 | in_message = as2.Message() 410 | _, _, mdn = in_message.parse( 411 | raw_out_message, 412 | find_org_cb=self.find_org, 413 | find_partner_cb=self.find_partner, 414 | find_message_cb=lambda x, y: False, 415 | ) 416 | 417 | # Parse the MDN 418 | out_mdn = as2.Mdn() 419 | status, detailed_status = out_mdn.parse( 420 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=lambda x, y: False 421 | ) 422 | 423 | self.assertEqual(status, "failed/Failure") 424 | self.assertEqual(detailed_status, "original-message-not-found") 425 | 426 | def test_unsigned_mdn_sent_error(self): 427 | """Test the case where a signed mdn was expected but unsigned mdn was returned.""" 428 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 429 | self.out_message = as2.Message(self.org, self.partner) 430 | self.out_message.build(self.test_data) 431 | 432 | # Parse the generated AS2 message as the partner 433 | raw_out_message = ( 434 | self.out_message.headers_str + b"\r\n" + self.out_message.content 435 | ) 436 | in_message = as2.Message() 437 | _, _, mdn = in_message.parse( 438 | raw_out_message, 439 | find_org_cb=self.find_org, 440 | find_partner_cb=self.find_partner, 441 | find_message_cb=lambda x, y: False, 442 | ) 443 | 444 | # Set the mdn sig alg and parse it 445 | self.partner.mdn_digest_alg = "sha256" 446 | out_mdn = as2.Mdn() 447 | status, detailed_status = out_mdn.parse( 448 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 449 | ) 450 | 451 | self.assertEqual(status, "failed/Failure") 452 | self.assertEqual( 453 | detailed_status, "Expected signed MDN but unsigned MDN returned" 454 | ) 455 | 456 | def test_non_matching_mic(self): 457 | """Test the case where a the mic in the mdn does not match the mic in the message.""" 458 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 459 | self.partner.sign = True 460 | self.out_message = as2.Message(self.org, self.partner) 461 | self.out_message.build(self.test_data) 462 | 463 | # Parse the generated AS2 message as the partner 464 | raw_out_message = ( 465 | self.out_message.headers_str + b"\r\n" + self.out_message.content 466 | ) 467 | in_message = as2.Message() 468 | _, _, mdn = in_message.parse( 469 | raw_out_message, 470 | find_org_cb=self.find_org, 471 | find_partner_cb=self.find_partner, 472 | find_message_cb=lambda x, y: False, 473 | ) 474 | 475 | # Set the mdn sig alg and parse it 476 | self.out_message.mic = b"dummy value" 477 | out_mdn = as2.Mdn() 478 | status, detailed_status = out_mdn.parse( 479 | mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=self.find_message 480 | ) 481 | 482 | self.assertEqual(status, "processed/warning") 483 | self.assertEqual(detailed_status, "Message Integrity check failed.") 484 | 485 | def test_missing_address_type(self): 486 | """Test the case where the address type is missing and only a receiver is provided.""" 487 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 488 | self.partner.sign = True 489 | self.out_message = as2.Message(self.org, self.partner) 490 | self.out_message.build(self.test_data) 491 | 492 | # Parse the generated AS2 message as the partner 493 | raw_out_message = ( 494 | self.out_message.headers_str + b"\r\n" + self.out_message.content 495 | ) 496 | in_message = as2.Message() 497 | _, _, mdn = in_message.parse( 498 | raw_out_message, 499 | find_org_cb=self.find_org, 500 | find_partner_cb=self.find_partner, 501 | find_message_cb=lambda x, y: False, 502 | ) 503 | 504 | # Remove the address type from the content. 505 | patched_content = mdn.content.replace( 506 | b"Original-Recipient: rfc822;", b"Original-Recipient:" 507 | ) 508 | patched_content = patched_content.replace( 509 | b"Final-Recipient: rfc822;", b"Final-Recipient:" 510 | ) 511 | 512 | out_mdn = as2.Mdn() 513 | out_mdn.payload = parse_mime(mdn.headers_str + b"\r\n" + patched_content) 514 | message_id, message_recipient = out_mdn.detect_mdn() 515 | 516 | self.assertEqual(message_recipient, self.partner.as2_name) 517 | 518 | def test_final_recipient_fallback(self): 519 | """Test the case where the original recipient is missing, but final recipient is provided.""" 520 | self.partner.mdn_mode = as2.SYNCHRONOUS_MDN 521 | self.partner.sign = True 522 | self.out_message = as2.Message(self.org, self.partner) 523 | self.out_message.build(self.test_data) 524 | 525 | # Parse the generated AS2 message as the partner 526 | raw_out_message = ( 527 | self.out_message.headers_str + b"\r\n" + self.out_message.content 528 | ) 529 | in_message = as2.Message() 530 | _, _, mdn = in_message.parse( 531 | raw_out_message, 532 | find_org_cb=self.find_org, 533 | find_partner_cb=self.find_partner, 534 | find_message_cb=lambda x, y: False, 535 | ) 536 | 537 | # Remove the address type from the content. 538 | patched_content = mdn.content.replace( 539 | b"Original-Recipient: rfc822; " 540 | + self.partner.as2_name.encode("utf-8") 541 | + b"\r\n", 542 | b"", 543 | ) 544 | 545 | out_mdn = as2.Mdn() 546 | out_mdn.payload = parse_mime(mdn.headers_str + b"\r\n" + patched_content) 547 | message_id, message_recipient = out_mdn.detect_mdn() 548 | 549 | self.assertEqual(message_recipient, self.partner.as2_name) 550 | 551 | def find_org(self, headers): 552 | return self.org 553 | 554 | def find_partner(self, headers): 555 | return self.partner 556 | 557 | def find_message(self, message_id, message_recipient): 558 | return self.out_message 559 | 560 | 561 | class SterlingIntegratorTest(Pyas2TestCase): 562 | def setUp(self): 563 | self.org = as2.Organization( 564 | as2_name="AS2 Server", 565 | sign_key=self.oldpyas2_private_key, 566 | sign_key_pass="password", 567 | decrypt_key=self.oldpyas2_private_key, 568 | decrypt_key_pass="password", 569 | ) 570 | self.partner = as2.Partner( 571 | as2_name="Sterling B2B Integrator", 572 | verify_cert=self.sb2bi_public_key, 573 | verify_cert_ca=self.sb2bi_public_ca, 574 | encrypt_cert=self.sb2bi_public_key, 575 | encrypt_cert_ca=self.sb2bi_public_ca, 576 | validate_certs=False, 577 | ) 578 | self.partner.load_verify_cert() 579 | self.partner.load_encrypt_cert() 580 | 581 | @pytest.mark.skip(reason="no way of currently testing this") 582 | def test_process_message(self): 583 | """Test processing message received from Sterling Integrator""" 584 | with open(os.path.join(TEST_DIR, "sb2bi_signed_cmp.msg"), "rb") as msg: 585 | as2message = as2.Message() 586 | status, exception, as2mdn = as2message.parse( 587 | msg.read(), 588 | lambda x: self.org, 589 | lambda y: self.partner, 590 | lambda x, y: False, 591 | ) 592 | self.assertEqual(status, "processed") 593 | 594 | def test_process_mdn(self): 595 | """Test processing mdn received from Sterling Integrator""" 596 | msg = as2.Message(sender=self.org, receiver=self.partner) 597 | msg.message_id = ( 598 | "151694007918.24690.7052273208458909245@ip-172-31-14-209.ec2.internal" 599 | ) 600 | 601 | as2mdn = as2.Mdn() 602 | # Parse the mdn and get the message status 603 | with open(os.path.join(TEST_DIR, "sb2bi_signed.mdn"), "rb") as mdn: 604 | status, detailed_status = as2mdn.parse(mdn.read(), lambda x, y: msg) 605 | self.assertEqual(status, "processed") 606 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------