From 871fa881d9cd8dcd6b76a7f6fed7aeb1490e743d Mon Sep 17 00:00:00 2001 From: Henrik Blidh Date: Sun, 10 Apr 2016 22:22:17 +0200 Subject: [PATCH] Version 0.3.5 Better certificate splitting methods. More extensive tests with mock. Documentation updated. Removed unnecessary Python 3 text conversions. --- .coveragerc | 5 ++ .travis.yml | 3 +- bankid/__init__.py | 4 +- bankid/certutils.py | 127 +++++++++++++++++++++++++++++++++++++++++++ bankid/client.py | 4 +- bankid/exceptions.py | 1 + bankid/testcert.py | 124 ------------------------------------------ docs/certutils.rst | 53 ++++++++++++++++++ docs/index.rst | 2 +- docs/testcert.rst | 19 ------- tests/test_client.py | 56 +++++++++++++------ 11 files changed, 232 insertions(+), 166 deletions(-) create mode 100644 bankid/certutils.py delete mode 100644 bankid/testcert.py create mode 100644 docs/certutils.rst delete mode 100644 docs/testcert.rst diff --git a/.coveragerc b/.coveragerc index ffa8370..d70264f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,3 +4,8 @@ source = pybankid include = */pybankid/* omit = */setup.py + +[report] +exclude_lines = + if 'requirementAlternatives' in kwargs + diff --git a/.travis.yml b/.travis.yml index 1ec87b4..23ed648 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,9 +21,10 @@ branches: - develop install: - "pip install pytest" + - "pip install mock" - "pip install pytest-cov" - "pip install python-coveralls" - - "pip install -r requirements.txt" + - "pip install -e ." script: py.test tests/ --cov bankid --cov-report term-missing after_success: - coveralls diff --git a/bankid/__init__.py b/bankid/__init__.py index a082bd5..e08ac3d 100644 --- a/bankid/__init__.py +++ b/bankid/__init__.py @@ -6,7 +6,7 @@ try: from .client import BankIDClient import bankid.exceptions as exceptions - from .testcert import create_bankid_test_server_cert_and_key + from .certutils import create_bankid_test_server_cert_and_key __all__ = ['BankIDClient', 'exceptions', 'create_bankid_test_server_cert_and_key', 'version'] except ImportError: @@ -21,7 +21,7 @@ # version. _version_major = 0 _version_minor = 3 -_version_patch = 4 +_version_patch = 5 # _version_extra = 'dev1' # _version_extra = 'a1' _version_extra = '' # Uncomment this for full releases diff --git a/bankid/certutils.py b/bankid/certutils.py new file mode 100644 index 0000000..f1cf3b6 --- /dev/null +++ b/bankid/certutils.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +:mod:`bankid.certutils` -- Certificate Utilities +================================================ + +Created by hbldh + +Created on 2016-04-10 + +""" + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import absolute_import + +import os +import tempfile +import subprocess +import requests + +_TEST_CERT_PASSWORD = 'qwerty123' +_TEST_CERT_URL = "https://www.bankid.com/assets/bankid/rp/FPTestcert2_20150818_102329.pfx" + + +def create_bankid_test_server_cert_and_key(destination_path): + """Fetch the P12 certificate from BankID servers, split it into + a certificate part and a key part and save them as separate files, + stored in PEM format. + + :param destination_path: The directory to save certificate and key files to. + :type destination_path: str + :returns: The path tuple ``(cert_path, key_path)``. + :rtype: tuple + + """ + + # Fetch P12 certificate and store in temporary folder. + cert_tmp_path = os.path.join(tempfile.gettempdir(), + os.path.basename(_TEST_CERT_URL)) + r = requests.get(_TEST_CERT_URL) + with open(cert_tmp_path, 'wb') as f: + f.write(r.content) + + certificate, key = split_certificate(cert_tmp_path, + destination_path, + password=_TEST_CERT_PASSWORD) + # Try to remove temporary file. + try: + os.remove(cert_tmp_path) + except: + pass + + # Return path tuples. + return certificate, key + + +def split_certificate(certificate_path, destination_folder, password=None): + """Splits a PKCS12 certificate into Base64-encoded DER certificate and key. + + This method splits a potentially password-protected + `PKCS12 `_ certificate + (format ``.p12`` or ``.pfx``) into one certificate and one key part, both in + `pem `_ + format. + + :returns: Tuple of certificate and key string data. + :rtype: tuple + + """ + try: + p = subprocess.Popen(["openssl", 'version'], stdout=subprocess.PIPE) + sout, serr = p.communicate() + if not sout.decode().lower().startswith('openssl'): + raise NotImplementedError( + "OpenSSL executable could not be found. " + "Splitting cannot be performed.") + except: + raise NotImplementedError( + "OpenSSL executable could not be found. " + "Splitting cannot be performed.") + + # Paths to output files. + out_cert_path = os.path.join(os.path.abspath( + os.path.expanduser(destination_folder)), 'certificate.pem') + out_key_path = os.path.join(os.path.abspath( + os.path.expanduser(destination_folder)), 'key.pem') + + # Use openssl for converting to pem format. + pipeline_1 = [ + 'openssl', 'pkcs12', + '-in', "{0}".format(certificate_path), + '-passin' if password is not None else '', + 'pass:{0}'.format(password) if password is not None else '', + '-out', "{0}".format(out_cert_path), + '-clcerts', '-nokeys' + ] + p = subprocess.Popen(list(filter(None, pipeline_1)), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + p.communicate() + pipeline_2 = [ + 'openssl', 'pkcs12', + '-in', "{0}".format(certificate_path), + '-passin' if password is not None else '', + 'pass:{0}'.format(password) if password is not None else '', + '-out', "{0}".format(out_key_path), + '-nocerts', '-nodes' + ] + p = subprocess.Popen(list(filter(None, pipeline_2)), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + p.communicate() + + # Return path tuples. + return out_cert_path, out_key_path + + +def main(): + paths = create_bankid_test_server_cert_and_key(os.path.expanduser('~')) + print('Saved certificate as {0}'.format(paths[0])) + print('Saved key as {0}'.format(paths[1])) + return paths + +if __name__ == "__main__": + main() diff --git a/bankid/client.py b/bankid/client.py index ba9c80e..4171590 100644 --- a/bankid/client.py +++ b/bankid/client.py @@ -5,6 +5,7 @@ ===================================== .. moduleauthor:: hbldh + Created on 2014-09-09, 16:55 """ @@ -106,7 +107,6 @@ def sign(self, user_visible_data, personal_number=None, **kwargs): warnings.warn("Requirement Alternatives option is not tested.", BankIDWarning) try: - out = self.client.service.Sign( userVisibleData=six.text_type(base64.b64encode(six.b(user_visible_data)), encoding='utf-8'), personalNumber=personal_number, **kwargs) @@ -197,7 +197,7 @@ def open(self, request): resp = self.requests_session.get(request.url, data=request.message, headers=request.headers) - result = six.BytesIO(six.b(resp.content.decode('utf-8'))) + result = six.BytesIO(resp.content) return result def send(self, request): diff --git a/bankid/exceptions.py b/bankid/exceptions.py index e5f438d..7adae57 100644 --- a/bankid/exceptions.py +++ b/bankid/exceptions.py @@ -5,6 +5,7 @@ =============================================== .. moduleauthor:: hbldh + Created on 2014-09-10, 08:29 """ diff --git a/bankid/testcert.py b/bankid/testcert.py deleted file mode 100644 index 2f5115c..0000000 --- a/bankid/testcert.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -:mod:`bankid.testcert` -- Test Certificate fetching -=================================================== - -.. moduleauthor:: hbldh -Created on 2014-09-09, 16:55 - -""" - -from __future__ import division -from __future__ import print_function -from __future__ import absolute_import - -import os -import tempfile -import subprocess -import sys -import requests - -_TEST_CERT_PASSWORD = 'qwerty123' -_TEST_CERT_URL = "https://www.bankid.com/assets/bankid/rp/FPTestcert2_20150818_102329.pfx" - - -def create_bankid_test_server_cert_and_key(destination_path): - """Fetch the P12 certificate from BankID servers, split it into - a certificate part and a key part and save them as separate files, - stored in PEM format. - - :param destination_path: The directory to save certificate and key files to. - :type destination_path: str - :returns: The path tuple ``(cert_path, key_path)``. - :rtype: tuple - - """ - if sys.platform == 'win32': - raise NotImplementedError( - "Test certificate fetching in Windows not supported. " - "See documentation for details.") - - certificate, key = split_test_cert_and_key() - - # Paths to output files. - out_cert_path = os.path.join(os.path.abspath( - os.path.expanduser(destination_path)), 'cert.pem') - out_key_path = os.path.join(os.path.abspath( - os.path.expanduser(destination_path)), 'key.pem') - - with open(out_cert_path, 'wt') as f: - f.write(certificate) - with open(out_key_path, 'wt') as f: - f.write(key) - - # Return path tuples. - return out_cert_path, out_key_path - - -def split_test_cert_and_key(): - """Fetch the P12 certificate from BankID servers, split it into - a certificate part and a key part and return the two components as text data. - - :returns: Tuple of certificate and key string data. - :rtype: tuple - - """ - # Paths to temporary files. - cert_tmp_path = os.path.join(tempfile.gettempdir(), os.path.basename(_TEST_CERT_URL)) - cert_conv_tmp_path = os.path.join(tempfile.gettempdir(), 'certificate.pem') - key_conv_tmp_path = os.path.join(tempfile.gettempdir(), 'key.pem') - - # Fetch P12 certificate and store in temporary folder. - r = requests.get(_TEST_CERT_URL) - with open(cert_tmp_path, 'wb') as f: - f.write(r.content) - - # Use openssl for converting to pem format. - pipeline_1 = [ - 'openssl', 'pkcs12', - '-in', "{0}".format(cert_tmp_path), - '-passin', 'pass:{0}'.format(_TEST_CERT_PASSWORD), - '-out', "{0}".format(cert_conv_tmp_path), - '-clcerts', '-nokeys' - ] - p = subprocess.Popen(pipeline_1, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - p.communicate() - pipeline_2 = [ - 'openssl', 'pkcs12', - '-in', "{0}".format(cert_tmp_path), - '-passin', 'pass:{0}'.format(_TEST_CERT_PASSWORD), - '-out', "{0}".format(key_conv_tmp_path), - '-nocerts', '-nodes' - ] - p = subprocess.Popen(pipeline_2, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - p.communicate() - - # Open the newly created PEM certificate and key in the temporary folder. - with open(cert_conv_tmp_path, 'rt') as f: - certificate = f.read() - with open(key_conv_tmp_path, 'rt') as f: - key = f.read() - - # Try to remove all temporary files. - try: - os.remove(cert_tmp_path) - os.remove(cert_conv_tmp_path) - os.remove(key_conv_tmp_path) - except: - pass - - return certificate, key - - -def main(): - paths = create_bankid_test_server_cert_and_key(os.path.expanduser('~')) - print('Saved certificate as {0}'.format(paths[0])) - print('Saved key as {0}'.format(paths[1])) - -if __name__ == "__main__": - main() diff --git a/docs/certutils.rst b/docs/certutils.rst new file mode 100644 index 0000000..2286a04 --- /dev/null +++ b/docs/certutils.rst @@ -0,0 +1,53 @@ +.. _certutils: + +Certificate methods +=================== + +Converting/splitting certificates +--------------------------------- + +To convert your production certificate from PKCS_12 format to two ``pem``, +ready to be used by PyBankID, one can do the following: + +.. code-block:: python + + In [1]: from bankid.certutils import split_certificate + + In [2]: split_certificate('/path/to/certificate.p12', + '/destination/folder/', + 'password_for_certificate_p12') + Out [2]: ('/destination/folder/certificate.pem', + '/destination/folder/key.pem') + +It can also be done via regular OpenSSL terminal calls: + +.. code-block:: bash + + openssl pkcs12 -in /path/to/certificate.p12 -passin pass:password_for_certificate_p12 -out /destination/folder/certificate.pem -clcerts -nokeys + openssl pkcs12 -in /path/to/certificate.p12 -passin pass:password_for_certificate_p12 -out /destination/folder/key.pem -nocerts -nodes + +.. note:: + This also removes the password from the private key in the certificate, + which is a requirement for using the PyBankID package in an automated way. + +Test server certificate +----------------------- + +There is a test certificate available on `BankID Technical Information webpage +`_, which can be used for +testing authorization and signing. The +:py:func:`bankid.certutils.create_bankid_test_server_cert_and_key` in the +:py:mod:`bankid.certutils` module fetches that test certificate, splits it +into one certificate and one key part and converts it from +`.p12 or .pfx `_ format to +`pem `_. +These can then be used for testing purposes, by sending in ``test_server=True`` +keyword in the :py:class:`~BankIDClient`. + + +API +--- + +.. automodule:: bankid.certutils + :members: + diff --git a/docs/index.rst b/docs/index.rst index b140985..a1cd214 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,7 +33,7 @@ the BankID servers. usage client exceptions - testcert + certutils Indices and tables diff --git a/docs/testcert.rst b/docs/testcert.rst deleted file mode 100644 index 38a634e..0000000 --- a/docs/testcert.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. _testcert: - -Test Certificate methods -======================== - -There is a test certificate available on `BankID Technical Information webpage -`_, which can be used for -testing authorization and signing. The methods in the :py:mod:`bankid.testcert` -module fetches that test certificate, splits it into one certificate and one key part and -converts it from `pxf `_ format to -`pem `_. - -.. note:: - It also removes the password from the private key in the certificate, - which is a requirement for using the PyBankID package in an automated way. - -.. automodule:: bankid.testcert - :members: - diff --git a/tests/test_client.py b/tests/test_client.py index c28ec45..1efa268 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -25,9 +25,13 @@ import uuid import pytest +try: + from unittest import mock +except: + import mock import bankid -import bankid.testcert +import bankid.certutils def _get_random_personal_number(): @@ -65,14 +69,16 @@ def digits_of(n): @pytest.fixture(scope="module") -def get_test_cert_and_key(): - return bankid.create_bankid_test_server_cert_and_key(tempfile.gettempdir()) +def cert_and_key(): + cert, key = bankid.create_bankid_test_server_cert_and_key(tempfile.gettempdir()) + return cert, key -def test_authentication_and_collect(): +def test_authentication_and_collect(cert_and_key): """Authenticate call and then collect with the returned orderRef UUID.""" - c = bankid.BankIDClient(certificates=get_test_cert_and_key(), test_server=True) + c = bankid.BankIDClient(certificates=cert_and_key, test_server=True) + assert 'appapi.test.bankid.com.pem' in c.verify_cert out = c.authenticate(_get_random_personal_number()) assert isinstance(out, dict) # UUID.__init__ performs the UUID compliance assertion. @@ -81,10 +87,10 @@ def test_authentication_and_collect(): assert collect_status.get('progressStatus') in ('OUTSTANDING_TRANSACTION', 'NO_CLIENT') -def test_sign_and_collect(): +def test_sign_and_collect(cert_and_key): """Sign call and then collect with the returned orderRef UUID.""" - c = bankid.BankIDClient(certificates=get_test_cert_and_key(), test_server=True) + c = bankid.BankIDClient(certificates=cert_and_key, test_server=True) out = c.sign("The data to be signed", _get_random_personal_number()) assert isinstance(out, dict) # UUID.__init__ performs the UUID compliance assertion. @@ -93,33 +99,49 @@ def test_sign_and_collect(): assert collect_status.get('progressStatus') in ('OUTSTANDING_TRANSACTION', 'NO_CLIENT') -def test_invalid_orderref_raises_error(): - c = bankid.BankIDClient(certificates=get_test_cert_and_key(), test_server=True) +def test_invalid_orderref_raises_error(cert_and_key): + c = bankid.BankIDClient(certificates=cert_and_key, test_server=True) with pytest.raises(bankid.exceptions.InvalidParametersError): collect_status = c.collect('invalid-uuid') -def test_already_in_progress_raises_error(): - c = bankid.client.BankIDClient(certificates=get_test_cert_and_key(), test_server=True) +def test_already_in_progress_raises_error(cert_and_key): + c = bankid.client.BankIDClient(certificates=cert_and_key, test_server=True) pn = _get_random_personal_number() out = c.authenticate(pn) with pytest.raises(bankid.exceptions.AlreadyInProgressError): out2 = c.authenticate(pn) -def test_file_sign_not_implemented(): - c = bankid.client.BankIDClient(certificates=get_test_cert_and_key(), test_server=True) +def test_file_sign_not_implemented(cert_and_key): + c = bankid.client.BankIDClient(certificates=cert_and_key, test_server=True) with pytest.raises(NotImplementedError): out = c.file_sign() -def test_test_cert_main(): - bankid.testcert.main() - assert os.path.exists(os.path.expanduser('~/cert.pem')) +def test_correct_prod_server_urls(cert_and_key): + bankid.client.Client.__init__ = mock.MagicMock(return_value=None) + c = bankid.client.BankIDClient(certificates=cert_and_key, test_server=False) + assert c.api_url == 'https://appapi.bankid.com/rp/v4' + assert c.wsdl_url == 'https://appapi.bankid.com/rp/v4?wsdl' + assert 'appapi.bankid.com.pem' in c.verify_cert + + +def test_correct_prod_server_urls_2(cert_and_key): + bankid.client.Client.__init__ = mock.MagicMock(return_value=None) + c = bankid.client.BankIDClient(certificates=cert_and_key) + assert c.api_url == 'https://appapi.bankid.com/rp/v4' + assert c.wsdl_url == 'https://appapi.bankid.com/rp/v4?wsdl' + assert 'appapi.bankid.com.pem' in c.verify_cert + + +def test_certutils_main(): + bankid.certutils.main() + assert os.path.exists(os.path.expanduser('~/certificate.pem')) assert os.path.exists(os.path.expanduser('~/key.pem')) try: - os.remove(os.path.expanduser('~/cert.pem')) + os.remove(os.path.expanduser('~/certificate.pem')) os.remove(os.path.expanduser('~/key.pem')) except: pass