django-x509-0.4.1/0000775000175000017500000000000013343702155014616 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509.egg-info/0000775000175000017500000000000013343702155020337 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509.egg-info/top_level.txt0000664000175000017500000000001413343702154023063 0ustar nemesisnemesis00000000000000django_x509 django-x509-0.4.1/django_x509.egg-info/dependency_links.txt0000664000175000017500000000000113343702154024404 0ustar nemesisnemesis00000000000000 django-x509-0.4.1/django_x509.egg-info/PKG-INFO0000664000175000017500000004270213343702154021440 0ustar nemesisnemesis00000000000000Metadata-Version: 1.1 Name: django-x509 Version: 0.4.1 Summary: Reusable django app to generate and manage x509 certificates Home-page: https://github.com/openwisp/django-x509 Author: Federico Capoano Author-email: f.capoano@cineca.it License: BSD Download-URL: https://github.com/openwisp/django-x509/releases Description: django-x509 =========== .. image:: https://travis-ci.org/openwisp/django-x509.svg :target: https://travis-ci.org/openwisp/django-x509 .. image:: https://coveralls.io/repos/openwisp/django-x509/badge.svg :target: https://coveralls.io/r/openwisp/django-x509 .. image:: https://requires.io/github/openwisp/django-x509/requirements.svg?branch=master :target: https://requires.io/github/openwisp/django-x509/requirements/?branch=master :alt: Requirements Status .. image:: https://badge.fury.io/py/django-x509.svg :target: http://badge.fury.io/py/django-x509 ------------ Simple reusable django app implementing x509 PKI certificates management. **Want to help OpenWISP?** `Find out how to help us grow here `_. ------------ .. contents:: **Table of Contents**: :backlinks: none :depth: 3 ------------ Current features ---------------- * CA generation * Import existing CAs * End entity certificate generation * Import existing certificates * Certificate revocation * CRL view (public or protected) * Possibility to specify x509 extensions on each certificate * Random serial numbers based on uuid4 integers (see `why is this a good idea `_) Project goals ------------- * provide a simple and reusable x509 PKI management django app * provide abstract models that can be imported and extended in larger django projects Dependencies ------------ * Python 2.7 or Python >= 3.4 * OpenSSL Install stable version from pypi -------------------------------- Install from pypi: .. code-block:: shell pip install django-x509 Install development version --------------------------- Install tarball: .. code-block:: shell pip install https://github.com/openwisp/django-x509/tarball/master Alternatively you can install via pip using git: .. code-block:: shell pip install -e git+git://github.com/openwisp/django-x509#egg=django-x509 If you want to contribute, install your cloned fork: .. code-block:: shell git clone git@github.com:/django-x509.git cd django-x509 python setup.py develop Setup (integrate in an existing django project) ----------------------------------------------- Add ``django_x509`` to ``INSTALLED_APPS``: .. code-block:: python INSTALLED_APPS = [ # other apps 'django_x509', ] Add the URLs to your main ``urls.py``: .. code-block:: python urlpatterns = [ # ... other urls in your project ... # django-x509 urls # keep the namespace argument unchanged url(r'^', include('django_x509.urls', namespace='x509')), ] Then run: .. code-block:: shell ./manage.py migrate Installing for development -------------------------- Install sqlite: .. code-block:: shell sudo apt-get install sqlite3 libsqlite3-dev Install your forked repo: .. code-block:: shell git clone git://github.com//django-x509 cd django-x509/ python setup.py develop Install test requirements: .. code-block:: shell pip install -r requirements-test.txt Create database: .. code-block:: shell cd tests/ ./manage.py migrate ./manage.py createsuperuser Launch development server: .. code-block:: shell ./manage.py runserver You can access the admin interface at http://127.0.0.1:8000/admin/. Run tests with: .. code-block:: shell ./runtests.py Install and run on docker -------------------------- Build from docker file: .. code-block:: shell sudo docker build -t openwisp/djangox509 . Run the docker container: .. code-block:: shell sudo docker run -it -p 8000:8000 openwisp/djangox509 Settings -------- ``DJANGO_X509_DEFAULT_CERT_VALIDITY`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``int`` | +--------------+-------------+ | **default**: | ``365`` | +--------------+-------------+ Default validity period (in days) when creating new x509 certificates. ``DJANGO_X509_DEFAULT_CA_VALIDITY`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``int`` | +--------------+-------------+ | **default**: | ``3650`` | +--------------+-------------+ Default validity period (in days) when creating new Certification Authorities. ``DJANGO_X509_DEFAULT_KEY_LENGTH`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``int`` | +--------------+-------------+ | **default**: | ``2048`` | +--------------+-------------+ Default key length for new CAs and new certificates. Must be one of the following values: * ``512`` * ``1024`` * ``2048`` * ``4096`` ``DJANGO_X509_DEFAULT_DIGEST_ALGORITHM`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``str`` | +--------------+-------------+ | **default**: | ``sha256`` | +--------------+-------------+ Default digest algorithm for new CAs and new certificates. Must be one of the following values: * ``sha1`` * ``sha224`` * ``sha256`` * ``sha384`` * ``sha512`` ``DJANGO_X509_CA_BASIC_CONSTRAINTS_CRITICAL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-----------+ | **type**: | ``bool`` | +--------------+-----------+ | **default**: | ``True`` | +--------------+-----------+ Whether the ``basicConstraint`` x509 extension must be flagged as critical when creating new CAs. ``DJANGO_X509_CA_BASIC_CONSTRAINTS_PATHLEN`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+---------------------+ | **type**: | ``int`` or ``None`` | +--------------+---------------------+ | **default**: | ``0`` | +--------------+---------------------+ Value of the ``pathLenConstraint`` of ``basicConstraint`` x509 extension used when creating new CAs. When this value is a positive ``int`` it represents the maximum number of non-self-issued intermediate certificates that may follow the generated certificate in a valid certification path. Set this value to ``None`` to avoid imposing any limit. ``DJANGO_X509_CA_KEYUSAGE_CRITICAL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+--------------------------+ | **type**: | ``bool`` | +--------------+--------------------------+ | **default**: | ``True`` | +--------------+--------------------------+ Whether the ``keyUsage`` x509 extension should be flagged as "critical" for new CAs. ``DJANGO_X509_CA_KEYUSAGE_VALUE`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+--------------------------+ | **type**: | ``str`` | +--------------+--------------------------+ | **default**: | ``cRLSign, keyCertSign`` | +--------------+--------------------------+ Value of the ``keyUsage`` x509 extension for new CAs. ``DJANGO_X509_CERT_KEYUSAGE_CRITICAL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+--------------------------+ | **type**: | ``bool`` | +--------------+--------------------------+ | **default**: | ``False`` | +--------------+--------------------------+ Whether the ``keyUsage`` x509 extension should be flagged as "critical" for new end-entity certificates. ``DJANGO_X509_CERT_KEYUSAGE_VALUE`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+---------------------------------------+ | **type**: | ``str`` | +--------------+---------------------------------------+ | **default**: | ``digitalSignature, keyEncipherment`` | +--------------+---------------------------------------+ Value of the ``keyUsage`` x509 extension for new end-entity certificates. ``DJANGO_X509_CRL_PROTECTED`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-----------+ | **type**: | ``bool`` | +--------------+-----------+ | **default**: | ``False`` | +--------------+-----------+ Whether the view for downloading Certificate Revocation Lists should be protected with authentication or not. Extending django-x509 --------------------- *django-x509* provides a set of models and admin classes which can be imported, extended and reused by third party apps. To extend *django-x509*, **you MUST NOT** add it to ``settings.INSTALLED_APPS``, but you must create your own app (which goes into ``settings.INSTALLED_APPS``), import the base classes from django-x509 and add your customizations. In order to help django find the static files and templates of *django-x509*, you need to perform the steps described below. 1. Install ``openwisp-utils`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Install (and add to the requirement of your project) `openwisp-utils `_:: pip install openwisp-utils 2. Add ``EXTENDED_APPS`` ~~~~~~~~~~~~~~~~~~~~~~~~ Add the following to your ``settings.py``: .. code-block:: python EXTENDED_APPS = ('django_x509',) 3. Add ``openwisp_utils.staticfiles.DependencyFinder`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add ``openwisp_utils.staticfiles.DependencyFinder`` to ``STATICFILES_FINDERS`` in your ``settings.py``: .. code-block:: python STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'openwisp_utils.staticfiles.DependencyFinder', ] 4. Add ``openwisp_utils.loaders.DependencyLoader`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your ``settings.py``: .. code-block:: python TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'OPTIONS': { 'loaders': [ 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', 'openwisp_utils.loaders.DependencyLoader', ], 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, } ] Extending models ~~~~~~~~~~~~~~~~ This example provides an example of how to extend the base models of *django-x509* by adding a relation to another django model named `Organization`. .. code-block:: python # models.py of your app from django.db import models from django_x509.base.models import AbstractCa, AbstractCert # the model ``organizations.Organization`` is omitted for brevity # if you are curious to see a real implementation, check out django-organizations class OrganizationMixin(models.Model): organization = models.ForeignKey('organizations.Organization') class Meta: abstract = True class Ca(OrganizationMixin, AbstractCa): class Meta(AbstractCa.Meta): abstract = False def clean(self): # your own validation logic here... pass class Cert(OrganizationMixin, AbstractCert): ca = models.ForeignKey(Ca) class Meta(AbstractCert.Meta): abstract = False def clean(self): # your own validation logic here... pass Extending the admin ~~~~~~~~~~~~~~~~~~~ Following the previous `Organization` example, you can avoid duplicating the admin code by importing the base admin classes and registering your models with. .. code-block:: python # admin.py of your app from django.contrib import admin from django_x509.base.admin import CaAdmin as BaseCaAdmin from django_x509.base.admin import CertAdmin as BaseCertAdmin from .models import Ca, Cert class CaAdmin(BaseCaAdmin): # extend/modify the default behaviour here pass class CertAdmin(BaseCertAdmin): # extend/modify the default behaviour here pass admin.site.register(Ca, CaAdmin) admin.site.register(Cert, CertAdmin) Contributing ------------ Please read the `OpenWISP contributing guidelines `_ and also keep in mind the following: 1. Announce your intentions in the `OpenWISP Mailing List `_ 2. Fork this repo and install it 3. Follow `PEP8, Style Guide for Python Code`_ 4. Write code 5. Write tests for your code 6. Ensure all tests pass 7. Ensure test coverage does not decrease 8. Document your changes 9. Send pull request .. _PEP8, Style Guide for Python Code: http://www.python.org/dev/peps/pep-0008/ Changelog --------- See `CHANGES `_. License ------- See `LICENSE `_. Support ------- See `OpenWISP Support Channels `_. Keywords: django,x509,pki,PEM,openwisp Platform: Platform Indipendent Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Web Environment Classifier: Topic :: Internet :: WWW/HTTP Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Framework :: Django Classifier: Topic :: Security :: Cryptography Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.4 django-x509-0.4.1/django_x509.egg-info/SOURCES.txt0000664000175000017500000000220113343702154022215 0ustar nemesisnemesis00000000000000CHANGES.rst LICENSE MANIFEST.in README.rst requirements.txt setup.cfg setup.py django_x509/__init__.py django_x509/admin.py django_x509/apps.py django_x509/models.py django_x509/settings.py django_x509/urls.py django_x509/utils.py django_x509/views.py django_x509.egg-info/PKG-INFO django_x509.egg-info/SOURCES.txt django_x509.egg-info/dependency_links.txt django_x509.egg-info/not-zip-safe django_x509.egg-info/requires.txt django_x509.egg-info/top_level.txt django_x509/base/__init__.py django_x509/base/admin.py django_x509/base/models.py django_x509/base/views.py django_x509/migrations/0001_initial.py django_x509/migrations/0002_certificate.py django_x509/migrations/0003_rename_organization_field.py django_x509/migrations/0004_auto_20171207_1450.py django_x509/migrations/0005_organizational_unit_name.py django_x509/migrations/__init__.py django_x509/static/django-x509/css/admin.css django_x509/static/django-x509/js/x509-admin.js django_x509/templates/admin/django_x509/change_form.html django_x509/tests/__init__.py django_x509/tests/test_admin.py django_x509/tests/test_ca.py django_x509/tests/test_cert.py django_x509/tests/test_helpers.pydjango-x509-0.4.1/django_x509.egg-info/not-zip-safe0000664000175000017500000000000113234215653022566 0ustar nemesisnemesis00000000000000 django-x509-0.4.1/django_x509.egg-info/requires.txt0000664000175000017500000000015013343702154022732 0ustar nemesisnemesis00000000000000six django<2.2,>=1.11 django-model-utils jsonfield cryptography<2.4.0,>=2.3.0 pyopenssl<18.1.0,>=17.5.0 django-x509-0.4.1/PKG-INFO0000664000175000017500000004270213343702155015720 0ustar nemesisnemesis00000000000000Metadata-Version: 1.1 Name: django-x509 Version: 0.4.1 Summary: Reusable django app to generate and manage x509 certificates Home-page: https://github.com/openwisp/django-x509 Author: Federico Capoano Author-email: f.capoano@cineca.it License: BSD Download-URL: https://github.com/openwisp/django-x509/releases Description: django-x509 =========== .. image:: https://travis-ci.org/openwisp/django-x509.svg :target: https://travis-ci.org/openwisp/django-x509 .. image:: https://coveralls.io/repos/openwisp/django-x509/badge.svg :target: https://coveralls.io/r/openwisp/django-x509 .. image:: https://requires.io/github/openwisp/django-x509/requirements.svg?branch=master :target: https://requires.io/github/openwisp/django-x509/requirements/?branch=master :alt: Requirements Status .. image:: https://badge.fury.io/py/django-x509.svg :target: http://badge.fury.io/py/django-x509 ------------ Simple reusable django app implementing x509 PKI certificates management. **Want to help OpenWISP?** `Find out how to help us grow here `_. ------------ .. contents:: **Table of Contents**: :backlinks: none :depth: 3 ------------ Current features ---------------- * CA generation * Import existing CAs * End entity certificate generation * Import existing certificates * Certificate revocation * CRL view (public or protected) * Possibility to specify x509 extensions on each certificate * Random serial numbers based on uuid4 integers (see `why is this a good idea `_) Project goals ------------- * provide a simple and reusable x509 PKI management django app * provide abstract models that can be imported and extended in larger django projects Dependencies ------------ * Python 2.7 or Python >= 3.4 * OpenSSL Install stable version from pypi -------------------------------- Install from pypi: .. code-block:: shell pip install django-x509 Install development version --------------------------- Install tarball: .. code-block:: shell pip install https://github.com/openwisp/django-x509/tarball/master Alternatively you can install via pip using git: .. code-block:: shell pip install -e git+git://github.com/openwisp/django-x509#egg=django-x509 If you want to contribute, install your cloned fork: .. code-block:: shell git clone git@github.com:/django-x509.git cd django-x509 python setup.py develop Setup (integrate in an existing django project) ----------------------------------------------- Add ``django_x509`` to ``INSTALLED_APPS``: .. code-block:: python INSTALLED_APPS = [ # other apps 'django_x509', ] Add the URLs to your main ``urls.py``: .. code-block:: python urlpatterns = [ # ... other urls in your project ... # django-x509 urls # keep the namespace argument unchanged url(r'^', include('django_x509.urls', namespace='x509')), ] Then run: .. code-block:: shell ./manage.py migrate Installing for development -------------------------- Install sqlite: .. code-block:: shell sudo apt-get install sqlite3 libsqlite3-dev Install your forked repo: .. code-block:: shell git clone git://github.com//django-x509 cd django-x509/ python setup.py develop Install test requirements: .. code-block:: shell pip install -r requirements-test.txt Create database: .. code-block:: shell cd tests/ ./manage.py migrate ./manage.py createsuperuser Launch development server: .. code-block:: shell ./manage.py runserver You can access the admin interface at http://127.0.0.1:8000/admin/. Run tests with: .. code-block:: shell ./runtests.py Install and run on docker -------------------------- Build from docker file: .. code-block:: shell sudo docker build -t openwisp/djangox509 . Run the docker container: .. code-block:: shell sudo docker run -it -p 8000:8000 openwisp/djangox509 Settings -------- ``DJANGO_X509_DEFAULT_CERT_VALIDITY`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``int`` | +--------------+-------------+ | **default**: | ``365`` | +--------------+-------------+ Default validity period (in days) when creating new x509 certificates. ``DJANGO_X509_DEFAULT_CA_VALIDITY`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``int`` | +--------------+-------------+ | **default**: | ``3650`` | +--------------+-------------+ Default validity period (in days) when creating new Certification Authorities. ``DJANGO_X509_DEFAULT_KEY_LENGTH`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``int`` | +--------------+-------------+ | **default**: | ``2048`` | +--------------+-------------+ Default key length for new CAs and new certificates. Must be one of the following values: * ``512`` * ``1024`` * ``2048`` * ``4096`` ``DJANGO_X509_DEFAULT_DIGEST_ALGORITHM`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``str`` | +--------------+-------------+ | **default**: | ``sha256`` | +--------------+-------------+ Default digest algorithm for new CAs and new certificates. Must be one of the following values: * ``sha1`` * ``sha224`` * ``sha256`` * ``sha384`` * ``sha512`` ``DJANGO_X509_CA_BASIC_CONSTRAINTS_CRITICAL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-----------+ | **type**: | ``bool`` | +--------------+-----------+ | **default**: | ``True`` | +--------------+-----------+ Whether the ``basicConstraint`` x509 extension must be flagged as critical when creating new CAs. ``DJANGO_X509_CA_BASIC_CONSTRAINTS_PATHLEN`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+---------------------+ | **type**: | ``int`` or ``None`` | +--------------+---------------------+ | **default**: | ``0`` | +--------------+---------------------+ Value of the ``pathLenConstraint`` of ``basicConstraint`` x509 extension used when creating new CAs. When this value is a positive ``int`` it represents the maximum number of non-self-issued intermediate certificates that may follow the generated certificate in a valid certification path. Set this value to ``None`` to avoid imposing any limit. ``DJANGO_X509_CA_KEYUSAGE_CRITICAL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+--------------------------+ | **type**: | ``bool`` | +--------------+--------------------------+ | **default**: | ``True`` | +--------------+--------------------------+ Whether the ``keyUsage`` x509 extension should be flagged as "critical" for new CAs. ``DJANGO_X509_CA_KEYUSAGE_VALUE`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+--------------------------+ | **type**: | ``str`` | +--------------+--------------------------+ | **default**: | ``cRLSign, keyCertSign`` | +--------------+--------------------------+ Value of the ``keyUsage`` x509 extension for new CAs. ``DJANGO_X509_CERT_KEYUSAGE_CRITICAL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+--------------------------+ | **type**: | ``bool`` | +--------------+--------------------------+ | **default**: | ``False`` | +--------------+--------------------------+ Whether the ``keyUsage`` x509 extension should be flagged as "critical" for new end-entity certificates. ``DJANGO_X509_CERT_KEYUSAGE_VALUE`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+---------------------------------------+ | **type**: | ``str`` | +--------------+---------------------------------------+ | **default**: | ``digitalSignature, keyEncipherment`` | +--------------+---------------------------------------+ Value of the ``keyUsage`` x509 extension for new end-entity certificates. ``DJANGO_X509_CRL_PROTECTED`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-----------+ | **type**: | ``bool`` | +--------------+-----------+ | **default**: | ``False`` | +--------------+-----------+ Whether the view for downloading Certificate Revocation Lists should be protected with authentication or not. Extending django-x509 --------------------- *django-x509* provides a set of models and admin classes which can be imported, extended and reused by third party apps. To extend *django-x509*, **you MUST NOT** add it to ``settings.INSTALLED_APPS``, but you must create your own app (which goes into ``settings.INSTALLED_APPS``), import the base classes from django-x509 and add your customizations. In order to help django find the static files and templates of *django-x509*, you need to perform the steps described below. 1. Install ``openwisp-utils`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Install (and add to the requirement of your project) `openwisp-utils `_:: pip install openwisp-utils 2. Add ``EXTENDED_APPS`` ~~~~~~~~~~~~~~~~~~~~~~~~ Add the following to your ``settings.py``: .. code-block:: python EXTENDED_APPS = ('django_x509',) 3. Add ``openwisp_utils.staticfiles.DependencyFinder`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add ``openwisp_utils.staticfiles.DependencyFinder`` to ``STATICFILES_FINDERS`` in your ``settings.py``: .. code-block:: python STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'openwisp_utils.staticfiles.DependencyFinder', ] 4. Add ``openwisp_utils.loaders.DependencyLoader`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your ``settings.py``: .. code-block:: python TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'OPTIONS': { 'loaders': [ 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', 'openwisp_utils.loaders.DependencyLoader', ], 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, } ] Extending models ~~~~~~~~~~~~~~~~ This example provides an example of how to extend the base models of *django-x509* by adding a relation to another django model named `Organization`. .. code-block:: python # models.py of your app from django.db import models from django_x509.base.models import AbstractCa, AbstractCert # the model ``organizations.Organization`` is omitted for brevity # if you are curious to see a real implementation, check out django-organizations class OrganizationMixin(models.Model): organization = models.ForeignKey('organizations.Organization') class Meta: abstract = True class Ca(OrganizationMixin, AbstractCa): class Meta(AbstractCa.Meta): abstract = False def clean(self): # your own validation logic here... pass class Cert(OrganizationMixin, AbstractCert): ca = models.ForeignKey(Ca) class Meta(AbstractCert.Meta): abstract = False def clean(self): # your own validation logic here... pass Extending the admin ~~~~~~~~~~~~~~~~~~~ Following the previous `Organization` example, you can avoid duplicating the admin code by importing the base admin classes and registering your models with. .. code-block:: python # admin.py of your app from django.contrib import admin from django_x509.base.admin import CaAdmin as BaseCaAdmin from django_x509.base.admin import CertAdmin as BaseCertAdmin from .models import Ca, Cert class CaAdmin(BaseCaAdmin): # extend/modify the default behaviour here pass class CertAdmin(BaseCertAdmin): # extend/modify the default behaviour here pass admin.site.register(Ca, CaAdmin) admin.site.register(Cert, CertAdmin) Contributing ------------ Please read the `OpenWISP contributing guidelines `_ and also keep in mind the following: 1. Announce your intentions in the `OpenWISP Mailing List `_ 2. Fork this repo and install it 3. Follow `PEP8, Style Guide for Python Code`_ 4. Write code 5. Write tests for your code 6. Ensure all tests pass 7. Ensure test coverage does not decrease 8. Document your changes 9. Send pull request .. _PEP8, Style Guide for Python Code: http://www.python.org/dev/peps/pep-0008/ Changelog --------- See `CHANGES `_. License ------- See `LICENSE `_. Support ------- See `OpenWISP Support Channels `_. Keywords: django,x509,pki,PEM,openwisp Platform: Platform Indipendent Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Web Environment Classifier: Topic :: Internet :: WWW/HTTP Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Framework :: Django Classifier: Topic :: Security :: Cryptography Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.4 django-x509-0.4.1/LICENSE0000664000175000017500000000272713177564257015651 0ustar nemesisnemesis00000000000000Copyright (c) 2015, Federico Capoano All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of django-x509, openwisp nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-x509-0.4.1/README.rst0000664000175000017500000003133613271375443016321 0ustar nemesisnemesis00000000000000django-x509 =========== .. image:: https://travis-ci.org/openwisp/django-x509.svg :target: https://travis-ci.org/openwisp/django-x509 .. image:: https://coveralls.io/repos/openwisp/django-x509/badge.svg :target: https://coveralls.io/r/openwisp/django-x509 .. image:: https://requires.io/github/openwisp/django-x509/requirements.svg?branch=master :target: https://requires.io/github/openwisp/django-x509/requirements/?branch=master :alt: Requirements Status .. image:: https://badge.fury.io/py/django-x509.svg :target: http://badge.fury.io/py/django-x509 ------------ Simple reusable django app implementing x509 PKI certificates management. **Want to help OpenWISP?** `Find out how to help us grow here `_. ------------ .. contents:: **Table of Contents**: :backlinks: none :depth: 3 ------------ Current features ---------------- * CA generation * Import existing CAs * End entity certificate generation * Import existing certificates * Certificate revocation * CRL view (public or protected) * Possibility to specify x509 extensions on each certificate * Random serial numbers based on uuid4 integers (see `why is this a good idea `_) Project goals ------------- * provide a simple and reusable x509 PKI management django app * provide abstract models that can be imported and extended in larger django projects Dependencies ------------ * Python 2.7 or Python >= 3.4 * OpenSSL Install stable version from pypi -------------------------------- Install from pypi: .. code-block:: shell pip install django-x509 Install development version --------------------------- Install tarball: .. code-block:: shell pip install https://github.com/openwisp/django-x509/tarball/master Alternatively you can install via pip using git: .. code-block:: shell pip install -e git+git://github.com/openwisp/django-x509#egg=django-x509 If you want to contribute, install your cloned fork: .. code-block:: shell git clone git@github.com:/django-x509.git cd django-x509 python setup.py develop Setup (integrate in an existing django project) ----------------------------------------------- Add ``django_x509`` to ``INSTALLED_APPS``: .. code-block:: python INSTALLED_APPS = [ # other apps 'django_x509', ] Add the URLs to your main ``urls.py``: .. code-block:: python urlpatterns = [ # ... other urls in your project ... # django-x509 urls # keep the namespace argument unchanged url(r'^', include('django_x509.urls', namespace='x509')), ] Then run: .. code-block:: shell ./manage.py migrate Installing for development -------------------------- Install sqlite: .. code-block:: shell sudo apt-get install sqlite3 libsqlite3-dev Install your forked repo: .. code-block:: shell git clone git://github.com//django-x509 cd django-x509/ python setup.py develop Install test requirements: .. code-block:: shell pip install -r requirements-test.txt Create database: .. code-block:: shell cd tests/ ./manage.py migrate ./manage.py createsuperuser Launch development server: .. code-block:: shell ./manage.py runserver You can access the admin interface at http://127.0.0.1:8000/admin/. Run tests with: .. code-block:: shell ./runtests.py Install and run on docker -------------------------- Build from docker file: .. code-block:: shell sudo docker build -t openwisp/djangox509 . Run the docker container: .. code-block:: shell sudo docker run -it -p 8000:8000 openwisp/djangox509 Settings -------- ``DJANGO_X509_DEFAULT_CERT_VALIDITY`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``int`` | +--------------+-------------+ | **default**: | ``365`` | +--------------+-------------+ Default validity period (in days) when creating new x509 certificates. ``DJANGO_X509_DEFAULT_CA_VALIDITY`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``int`` | +--------------+-------------+ | **default**: | ``3650`` | +--------------+-------------+ Default validity period (in days) when creating new Certification Authorities. ``DJANGO_X509_DEFAULT_KEY_LENGTH`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``int`` | +--------------+-------------+ | **default**: | ``2048`` | +--------------+-------------+ Default key length for new CAs and new certificates. Must be one of the following values: * ``512`` * ``1024`` * ``2048`` * ``4096`` ``DJANGO_X509_DEFAULT_DIGEST_ALGORITHM`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-------------+ | **type**: | ``str`` | +--------------+-------------+ | **default**: | ``sha256`` | +--------------+-------------+ Default digest algorithm for new CAs and new certificates. Must be one of the following values: * ``sha1`` * ``sha224`` * ``sha256`` * ``sha384`` * ``sha512`` ``DJANGO_X509_CA_BASIC_CONSTRAINTS_CRITICAL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-----------+ | **type**: | ``bool`` | +--------------+-----------+ | **default**: | ``True`` | +--------------+-----------+ Whether the ``basicConstraint`` x509 extension must be flagged as critical when creating new CAs. ``DJANGO_X509_CA_BASIC_CONSTRAINTS_PATHLEN`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+---------------------+ | **type**: | ``int`` or ``None`` | +--------------+---------------------+ | **default**: | ``0`` | +--------------+---------------------+ Value of the ``pathLenConstraint`` of ``basicConstraint`` x509 extension used when creating new CAs. When this value is a positive ``int`` it represents the maximum number of non-self-issued intermediate certificates that may follow the generated certificate in a valid certification path. Set this value to ``None`` to avoid imposing any limit. ``DJANGO_X509_CA_KEYUSAGE_CRITICAL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+--------------------------+ | **type**: | ``bool`` | +--------------+--------------------------+ | **default**: | ``True`` | +--------------+--------------------------+ Whether the ``keyUsage`` x509 extension should be flagged as "critical" for new CAs. ``DJANGO_X509_CA_KEYUSAGE_VALUE`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+--------------------------+ | **type**: | ``str`` | +--------------+--------------------------+ | **default**: | ``cRLSign, keyCertSign`` | +--------------+--------------------------+ Value of the ``keyUsage`` x509 extension for new CAs. ``DJANGO_X509_CERT_KEYUSAGE_CRITICAL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+--------------------------+ | **type**: | ``bool`` | +--------------+--------------------------+ | **default**: | ``False`` | +--------------+--------------------------+ Whether the ``keyUsage`` x509 extension should be flagged as "critical" for new end-entity certificates. ``DJANGO_X509_CERT_KEYUSAGE_VALUE`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+---------------------------------------+ | **type**: | ``str`` | +--------------+---------------------------------------+ | **default**: | ``digitalSignature, keyEncipherment`` | +--------------+---------------------------------------+ Value of the ``keyUsage`` x509 extension for new end-entity certificates. ``DJANGO_X509_CRL_PROTECTED`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------+-----------+ | **type**: | ``bool`` | +--------------+-----------+ | **default**: | ``False`` | +--------------+-----------+ Whether the view for downloading Certificate Revocation Lists should be protected with authentication or not. Extending django-x509 --------------------- *django-x509* provides a set of models and admin classes which can be imported, extended and reused by third party apps. To extend *django-x509*, **you MUST NOT** add it to ``settings.INSTALLED_APPS``, but you must create your own app (which goes into ``settings.INSTALLED_APPS``), import the base classes from django-x509 and add your customizations. In order to help django find the static files and templates of *django-x509*, you need to perform the steps described below. 1. Install ``openwisp-utils`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Install (and add to the requirement of your project) `openwisp-utils `_:: pip install openwisp-utils 2. Add ``EXTENDED_APPS`` ~~~~~~~~~~~~~~~~~~~~~~~~ Add the following to your ``settings.py``: .. code-block:: python EXTENDED_APPS = ('django_x509',) 3. Add ``openwisp_utils.staticfiles.DependencyFinder`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add ``openwisp_utils.staticfiles.DependencyFinder`` to ``STATICFILES_FINDERS`` in your ``settings.py``: .. code-block:: python STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'openwisp_utils.staticfiles.DependencyFinder', ] 4. Add ``openwisp_utils.loaders.DependencyLoader`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your ``settings.py``: .. code-block:: python TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'OPTIONS': { 'loaders': [ 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', 'openwisp_utils.loaders.DependencyLoader', ], 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, } ] Extending models ~~~~~~~~~~~~~~~~ This example provides an example of how to extend the base models of *django-x509* by adding a relation to another django model named `Organization`. .. code-block:: python # models.py of your app from django.db import models from django_x509.base.models import AbstractCa, AbstractCert # the model ``organizations.Organization`` is omitted for brevity # if you are curious to see a real implementation, check out django-organizations class OrganizationMixin(models.Model): organization = models.ForeignKey('organizations.Organization') class Meta: abstract = True class Ca(OrganizationMixin, AbstractCa): class Meta(AbstractCa.Meta): abstract = False def clean(self): # your own validation logic here... pass class Cert(OrganizationMixin, AbstractCert): ca = models.ForeignKey(Ca) class Meta(AbstractCert.Meta): abstract = False def clean(self): # your own validation logic here... pass Extending the admin ~~~~~~~~~~~~~~~~~~~ Following the previous `Organization` example, you can avoid duplicating the admin code by importing the base admin classes and registering your models with. .. code-block:: python # admin.py of your app from django.contrib import admin from django_x509.base.admin import CaAdmin as BaseCaAdmin from django_x509.base.admin import CertAdmin as BaseCertAdmin from .models import Ca, Cert class CaAdmin(BaseCaAdmin): # extend/modify the default behaviour here pass class CertAdmin(BaseCertAdmin): # extend/modify the default behaviour here pass admin.site.register(Ca, CaAdmin) admin.site.register(Cert, CertAdmin) Contributing ------------ Please read the `OpenWISP contributing guidelines `_ and also keep in mind the following: 1. Announce your intentions in the `OpenWISP Mailing List `_ 2. Fork this repo and install it 3. Follow `PEP8, Style Guide for Python Code`_ 4. Write code 5. Write tests for your code 6. Ensure all tests pass 7. Ensure test coverage does not decrease 8. Document your changes 9. Send pull request .. _PEP8, Style Guide for Python Code: http://www.python.org/dev/peps/pep-0008/ Changelog --------- See `CHANGES `_. License ------- See `LICENSE `_. Support ------- See `OpenWISP Support Channels `_. django-x509-0.4.1/setup.py0000664000175000017500000000421313177564257016346 0ustar nemesisnemesis00000000000000#!/usr/bin/env python import os import sys from setuptools import find_packages, setup from django_x509 import get_version def get_install_requires(): """ parse requirements.txt, ignore links, exclude comments """ requirements = [] for line in open('requirements.txt').readlines(): # skip to next iteration if comment or empty line if line.startswith('#') or line == '' or line.startswith('http') or line.startswith('git'): continue # add line to requirements requirements.append(line) return requirements if sys.argv[-1] == 'publish': # delete any *.pyc, *.pyo and __pycache__ os.system('find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf') os.system("python setup.py sdist bdist_wheel") os.system("twine upload -s dist/*") os.system("rm -rf dist build") args = {'version': get_version()} print("You probably want to also tag the version now:") print(" git tag -a %(version)s -m 'version %(version)s'" % args) print(" git push --tags") sys.exit() setup( name='django-x509', version=get_version(), license='BSD', author='Federico Capoano', author_email='f.capoano@cineca.it', description='Reusable django app to generate and manage x509 certificates', long_description=open('README.rst').read(), url='https://github.com/openwisp/django-x509', download_url='https://github.com/openwisp/django-x509/releases', platforms=['Platform Indipendent'], keywords=['django', 'x509', 'pki', 'PEM', 'openwisp'], packages=find_packages(exclude=['tests', 'docs']), include_package_data=True, zip_safe=False, install_requires=get_install_requires(), classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: Web Environment', 'Topic :: Internet :: WWW/HTTP', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Framework :: Django', 'Topic :: Security :: Cryptography', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', ] ) django-x509-0.4.1/MANIFEST.in0000664000175000017500000000040313177564257016367 0ustar nemesisnemesis00000000000000include LICENSE include README.rst include CHANGES.rst include requirements.txt recursive-include django_x509 * recursive-exclude * *.pyc recursive-exclude * *.swp recursive-exclude * __pycache__ recursive-exclude * *.db recursive-exclude * local_settings.py django-x509-0.4.1/CHANGES.rst0000664000175000017500000000714213343701754016430 0ustar nemesisnemesis00000000000000Changelog ========= Version 0.4.1 [2018-09-05] -------------------------- * [admin] Fixed UI bug that prevented changing Cert and CA * [requirements] cryptography>=2.3.0,<2.4.0 * [requirements] pyopenssl>=17.5.0,<18.1.0 * `#41 `_: [requirements] Added support for django 2.1 * [admin] Fixed involuntary permanent modification of field list Version 0.4.0 [2018-02-19] -------------------------- * `#24 `_: [qa] Added django 2.0 & dropped django 1.10 * `#25 `_: [admin] Automatically select ``certificate`` and ``private_key`` on click * `#33 `_: [models] Added ``organizational_unit_name`` in ``Cert`` and ``Ca`` Version 0.3.4 [2017-12-20] -------------------------- * [admin] Removed ``serial_number`` from certificate list Version 0.3.3 [2017-12-20] -------------------------- * [models] Reimplemented serial numbers as UUID integers * [UX] Import vs New javascript switcher Version 0.3.2 [2017-12-06] -------------------------- * [requirements] upgraded pyopenssl to 17.5.0 and cryptography to 2.2.0 * [models] Fixed uncaught exception when imported PEM ``certificate`` or ``private_key`` is invalid Version 0.3.1 [2017-12-01] -------------------------- * temporarily downgraded cryptography and pyopenssl versions to avoid segmentation faults Version 0.3.0 [2017-11-03] -------------------------- * [models] Avoided possible double insertion in ``Base.save`` * [requirements] pyopenssl>=17.1.0,<17.4.0 * [admin] Fixed preformatted look of certificate and private-key fields * [models] Allow importing certs with invalid country codes * [models] Allow importing certificate with empty common name * [tests] Updated data for import test to fix pyOpenSSL issue * [models] Renamed ``organization`` field to ``organization_name`` Version 0.2.4 [2017-07-04] -------------------------- * [models] added ``digest`` argument to ``CRL.export`` * [requirements] pyopenssl>=17.1.0,<17.2.0 Version 0.2.3 [2017-05-15] -------------------------- * [migrations] Updated ``validity_start`` on ``Cert`` model Version 0.2.2 [2017-05-11] -------------------------- * [models] Set ``validity_start`` to 1 day before the current date (at 00:00) Version 0.2.1 [2017-05-02] -------------------------- * [django] added support for django 1.11 Version 0.2.0 [2017-01-11] -------------------------- * [models] improved reusability by providing abstract models * [admin] improved reusability by providing abstract admin classes * [views] provided a base view that can be reused by third party apps * [docs] documented how to extend models and admin * [docs] documented hard dependencies Version 0.1.3 [2016-09-22] -------------------------- * [model] avoid import error if any imported field is ``NULL`` * [admin] added ``serial_number`` to ``list_display`` in ``Cert`` admin * [model] avoid exception if x509 subject attributes are empty Version 0.1.2 [2016-09-08] -------------------------- * improved general ``verbose_name`` of the app * added official compatibility with django 1.10 * [admin] show link to CA in cert admin * [admin] added ``key_length`` and ``digest`` to available filters Version 0.1.1 [2016-08-03] -------------------------- * fixed x509 certificate version * renamed ``public_key`` field to more appropiate ``certificate`` * show x509 text dump in admin when editing objects Version 0.1 [2016-07-18] ------------------------ * CA and end entity certificate generation * import existing certificates * x509 extensions * revocation * CRL django-x509-0.4.1/setup.cfg0000664000175000017500000000024113343702155016434 0ustar nemesisnemesis00000000000000[bdist_wheel] universal = 1 [isort] known_third_party = six known_first_party = django_x509 default_section = THIRDPARTY [egg_info] tag_build = tag_date = 0 django-x509-0.4.1/django_x509/0000775000175000017500000000000013343702155016645 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/models.py0000664000175000017500000000045213177564257020521 0ustar nemesisnemesis00000000000000from .base.models import AbstractCa, AbstractCert class Ca(AbstractCa): """ Concrete Ca model """ class Meta(AbstractCa.Meta): abstract = False class Cert(AbstractCert): """ Concrete Cert model """ class Meta(AbstractCert.Meta): abstract = False django-x509-0.4.1/django_x509/tests/0000775000175000017500000000000013343702155020007 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/tests/test_admin.py0000664000175000017500000001225413343027425022514 0ustar nemesisnemesis00000000000000from django.contrib.admin.sites import AdminSite from django.test import TestCase from ..admin import CaAdmin, CertAdmin from ..models import Ca, Cert from .test_helpers import MessagingRequest class MockSuperUser: def has_perm(self, perm): return True request = MessagingRequest() request.user = MockSuperUser() ca_fields = ['operation_type', 'name', 'notes', 'key_length', 'digest', 'validity_start', 'validity_end', 'country_code', 'state', 'city', 'organization_name', 'organizational_unit_name', 'email', 'common_name', 'extensions', 'serial_number', 'certificate', 'private_key'] cert_fields = ['operation_type', 'name', 'ca', 'notes', 'key_length', 'digest', 'validity_start', 'validity_end', 'country_code', 'state', 'city', 'organization_name', 'organizational_unit_name', 'email', 'common_name', 'extensions', 'serial_number', 'certificate', 'private_key'] ca_readonly = ['key_length', 'digest', 'validity_start', 'validity_end', 'country_code', 'state', 'city', 'organization_name', 'organizational_unit_name', 'email', 'common_name', 'serial_number', 'certificate', 'private_key', 'created', 'modified'] cert_readonly = ['revoked', 'revoked_at', 'created', 'modified', 'created', 'modified', 'created', 'modified', 'created', 'modified', 'created', 'modified', 'created', 'modified'] class ModelAdminTests(TestCase): def setUp(self): self.ca = Ca.objects.create() self.cert = Cert.objects.create(ca_id=self.ca.id) self.cert.ca = self.ca self.site = AdminSite() def test_modeladmin_str_ca(self): ma = CaAdmin(Ca, self.site) self.assertEqual(str(ma), 'django_x509.CaAdmin') def test_modeladmin_str_certr(self): ma = CertAdmin(Cert, self.site) self.assertEqual(str(ma), 'django_x509.CertAdmin') def test_default_fields_ca(self): ma = CaAdmin(Ca, self.site) self.assertEqual(list(ma.get_form(request).base_fields), ca_fields) ca_fields.insert(len(ca_fields), 'created') ca_fields.insert(len(ca_fields), 'modified') self.assertEqual(list(ma.get_fields(request)), ca_fields) index = ca_fields.index('extensions') ca_fields.remove('extensions') self.assertEqual(list(ma.get_fields(request, self.ca)), ca_fields) ca_fields.insert(index, 'extensions') def test_default_fields_cert(self): ma = CertAdmin(Cert, self.site) self.assertEqual(list(ma.get_form(request).base_fields), cert_fields) cert_fields.insert(4, 'revoked') cert_fields.insert(5, 'revoked_at') cert_fields.insert(len(cert_fields), 'created') cert_fields.insert(len(cert_fields), 'modified') self.assertEqual(list(ma.get_fields(request)), cert_fields) index = cert_fields.index('extensions') cert_fields.remove('extensions') self.assertEqual(list(ma.get_fields(request, self.cert)), cert_fields) cert_fields.insert(index, 'extensions') def test_default_fieldsets_ca(self): ma = CaAdmin(Ca, self.site) self.assertEqual(ma.get_fieldsets(request), [(None, {'fields': ca_fields})]) def test_default_fieldsets_cert(self): ma = CertAdmin(Cert, self.site) self.assertEqual(ma.get_fieldsets(request), [(None, {'fields': cert_fields})]) def test_readonly_fields_Ca(self): ma = CaAdmin(Ca, self.site) self.assertEqual(ma.get_readonly_fields(request), ('created', 'modified')) self.assertEqual(ma.get_readonly_fields(request, self.ca), tuple(ca_readonly)) ca_readonly.remove('created') ca_readonly.remove('modified') def test_readonly_fields_Cert(self): ma = CertAdmin(Cert, self.site) self.assertEqual(ma.get_readonly_fields(request), cert_readonly) ca_readonly.append('ca') self.assertEqual(ma.get_readonly_fields(request, self.cert), tuple(ca_readonly + cert_readonly)) def test_ca_url(self): ma = CertAdmin(Cert, self.site) self.assertEqual(ma.ca_url(self.cert), "") def test_revoke_action(self): ma = CertAdmin(Cert, self.site) ma.revoke_action(request, [self.cert]) m = list(request.get_messages()) self.assertEqual(len(m), 1) self.assertEqual(str(m[0]), '1 certificate was revoked.') django-x509-0.4.1/django_x509/tests/__init__.py0000664000175000017500000000270513177564257022142 0ustar nemesisnemesis00000000000000""" test utilities shared among test classes these mixins are reused also in openwisp2 change with care. """ class TestX509Mixin(object): def _create_ca(self, **kwargs): options = dict(name='Test CA', key_length='2048', digest='sha256', country_code='IT', state='RM', city='Rome', organization_name='OpenWISP', email='test@test.com', common_name='openwisp.org', extensions=[]) options.update(kwargs) ca = self.ca_model(**options) ca.full_clean() ca.save() return ca def _create_cert(self, **kwargs): options = dict(name='TestCert', ca=None, key_length='2048', digest='sha256', country_code='IT', state='RM', city='Rome', organization_name='Test', email='test@test.com', common_name='openwisp.org', extensions=[]) options.update(kwargs) # auto create CA if not supplied if not options.get('ca'): options['ca'] = self._create_ca() cert = self.cert_model(**options) cert.full_clean() cert.save() return cert django-x509-0.4.1/django_x509/tests/test_ca.py0000664000175000017500000005477513343026455022027 0ustar nemesisnemesis00000000000000from datetime import datetime, timedelta from django.core.exceptions import ValidationError from django.test import TestCase from django.urls import reverse from django.utils import timezone from OpenSSL import crypto from . import TestX509Mixin from .. import settings as app_settings from ..base.models import generalized_time from ..models import Ca, Cert class TestCa(TestX509Mixin, TestCase): """ tests for Ca model """ ca_model = Ca cert_model = Cert def _prepare_revoked(self): ca = self._create_ca() crl = crypto.load_crl(crypto.FILETYPE_PEM, ca.crl) self.assertIsNone(crl.get_revoked()) cert = self._create_cert(ca=ca) cert.revoke() return (ca, cert) import_certificate = """ -----BEGIN CERTIFICATE----- MIIB4zCCAY2gAwIBAwIDAeJAMA0GCSqGSIb3DQEBBQUAMHcxCzAJBgNVBAYTAlVT MQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwE QUNNRTEfMB0GCSqGSIb3DQEJARYQY29udGFjdEBhY21lLmNvbTETMBEGA1UEAwwK aW1wb3J0dGVzdDAiGA8yMDE1MDEwMTAwMDAwMFoYDzIwMjAwMTAxMDAwMDAwWjB3 MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz Y28xDTALBgNVBAoMBEFDTUUxHzAdBgkqhkiG9w0BCQEWEGNvbnRhY3RAYWNtZS5j b20xEzARBgNVBAMMCmltcG9ydHRlc3QwXDANBgkqhkiG9w0BAQEFAANLADBIAkEA v42Y9u9pYUiFRb36lwqdLmG8hCjl0g0HlMo2WqvHCTLk2CJvprBEuggSnaRCAmG9 ipCIds/ggaJ/w4KqJabNQQIDAQABMA0GCSqGSIb3DQEBBQUAA0EAAfEPPqbY1TLw 6IXNVelAXKxUp2f8FYCnlb0pQ3tswvefpad3h3oHrI2RGkIsM70axo7dAEk05Tj0 Zt3jXRLGAQ== -----END CERTIFICATE----- """ import_private_key = """ -----BEGIN PRIVATE KEY----- MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAv42Y9u9pYUiFRb36 lwqdLmG8hCjl0g0HlMo2WqvHCTLk2CJvprBEuggSnaRCAmG9ipCIds/ggaJ/w4Kq JabNQQIDAQABAkEAqpB3CEqeVxWwNi24GQ5Gb6pvpm6UVblsary0MYCLtk+jK6fg KCptUIryQ4cblZF54y3+wrLzJ9LUOStkk10DwQIhAPItbg5PqSZTCE/Ql20jUggo BHpXO7FI157oMxXnBJtVAiEAynx4ocYpgVtmJ9iSooZRtPp9ullEdUtU2pedSgY6 oj0CIHtcBs6FZ20dKIO3hhrSvgtnjvhejQp+R08rijIi7ibNAiBUOhR/zosjSN6k gnz0aAUC0BOOeWV1mQFR8DE4QoEPTQIhAIdGrho1hsZ3Cs7mInJiLLhh4zwnndQx WRyKPvMvJzWT -----END PRIVATE KEY----- """ def test_new(self): ca = self._create_ca() self.assertNotEqual(ca.certificate, '') self.assertNotEqual(ca.private_key, '') cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca.certificate) self.assertEqual(int(cert.get_serial_number()), int(ca.serial_number)) subject = cert.get_subject() self.assertEqual(subject.countryName, ca.country_code) self.assertEqual(subject.stateOrProvinceName, ca.state) self.assertEqual(subject.localityName, ca.city) self.assertEqual(subject.organizationName, ca.organization_name) self.assertEqual(subject.emailAddress, ca.email) self.assertEqual(subject.commonName, ca.common_name) issuer = cert.get_issuer() self.assertEqual(issuer.countryName, ca.country_code) self.assertEqual(issuer.stateOrProvinceName, ca.state) self.assertEqual(issuer.localityName, ca.city) self.assertEqual(issuer.organizationName, ca.organization_name) self.assertEqual(issuer.emailAddress, ca.email) self.assertEqual(issuer.commonName, ca.common_name) # ensure version is 3 self.assertEqual(cert.get_version(), 2) # basic constraints e = cert.get_extension(0) self.assertEqual(e.get_critical(), 1) self.assertEqual(e.get_short_name().decode(), 'basicConstraints') self.assertEqual(e.get_data(), b'0\x06\x01\x01\xff\x02\x01\x00') def test_x509_property(self): ca = self._create_ca() cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca.certificate) self.assertEqual(ca.x509.get_subject(), cert.get_subject()) self.assertEqual(ca.x509.get_issuer(), cert.get_issuer()) def test_x509_property_none(self): self.assertIsNone(Ca().x509) def test_pkey_property(self): ca = self._create_ca() self.assertIsInstance(ca.pkey, crypto.PKey) def test_pkey_property_none(self): self.assertIsNone(Ca().pkey) def test_default_validity_end(self): ca = Ca() self.assertEqual(ca.validity_end.year, datetime.now().year + 10) def test_default_validity_start(self): ca = Ca() expected = datetime.now() - timedelta(days=1) self.assertEqual(ca.validity_start.year, expected.year) self.assertEqual(ca.validity_start.month, expected.month) self.assertEqual(ca.validity_start.day, expected.day) self.assertEqual(ca.validity_start.hour, 0) self.assertEqual(ca.validity_start.minute, 0) self.assertEqual(ca.validity_start.second, 0) def test_import_ca(self): ca = Ca(name='ImportTest') ca.certificate = self.import_certificate ca.private_key = self.import_private_key ca.full_clean() ca.save() cert = ca.x509 # verify attributes self.assertEqual(cert.get_serial_number(), 123456) subject = cert.get_subject() self.assertEqual(subject.countryName, 'US') self.assertEqual(subject.stateOrProvinceName, 'CA') self.assertEqual(subject.localityName, 'San Francisco') self.assertEqual(subject.organizationName, 'ACME') self.assertEqual(subject.emailAddress, 'contact@acme.com') self.assertEqual(subject.commonName, 'importtest') issuer = cert.get_issuer() self.assertEqual(issuer.countryName, 'US') self.assertEqual(issuer.stateOrProvinceName, 'CA') self.assertEqual(issuer.localityName, 'San Francisco') self.assertEqual(issuer.organizationName, 'ACME') self.assertEqual(issuer.emailAddress, 'contact@acme.com') self.assertEqual(issuer.commonName, 'importtest') # verify field attribtues self.assertEqual(ca.key_length, '512') self.assertEqual(ca.digest, 'sha1') start = timezone.make_aware(datetime.strptime('20150101000000Z', generalized_time)) self.assertEqual(ca.validity_start, start) end = timezone.make_aware(datetime.strptime('20200101000000Z', generalized_time)) self.assertEqual(ca.validity_end, end) self.assertEqual(ca.country_code, 'US') self.assertEqual(ca.state, 'CA') self.assertEqual(ca.city, 'San Francisco') self.assertEqual(ca.organization_name, 'ACME') self.assertEqual(ca.email, 'contact@acme.com') self.assertEqual(ca.common_name, 'importtest') self.assertEqual(ca.name, 'ImportTest') self.assertEqual(int(ca.serial_number), 123456) # ensure version is 3 self.assertEqual(cert.get_version(), 3) ca.delete() # test auto name ca = Ca(certificate=self.import_certificate, private_key=self.import_private_key) ca.full_clean() ca.save() self.assertEqual(ca.name, 'importtest') def test_import_private_key_empty(self): ca = Ca(name='ImportTest') ca.certificate = self.import_certificate try: ca.full_clean() except ValidationError as e: # verify error message self.assertIn('importing an existing certificate', str(e)) else: self.fail('ValidationError not raised') def test_basic_constraints_not_critical(self): setattr(app_settings, 'CA_BASIC_CONSTRAINTS_CRITICAL', False) ca = self._create_ca() e = ca.x509.get_extension(0) self.assertEqual(e.get_critical(), 0) setattr(app_settings, 'CA_BASIC_CONSTRAINTS_CRITICAL', True) def test_basic_constraints_pathlen(self): setattr(app_settings, 'CA_BASIC_CONSTRAINTS_PATHLEN', 2) ca = self._create_ca() e = ca.x509.get_extension(0) self.assertEqual(e.get_data(), b'0\x06\x01\x01\xff\x02\x01\x02') setattr(app_settings, 'CA_BASIC_CONSTRAINTS_PATHLEN', 0) def test_basic_constraints_pathlen_none(self): setattr(app_settings, 'CA_BASIC_CONSTRAINTS_PATHLEN', None) ca = self._create_ca() e = ca.x509.get_extension(0) self.assertEqual(e.get_data(), b'0\x03\x01\x01\xff') setattr(app_settings, 'CA_BASIC_CONSTRAINTS_PATHLEN', 0) def test_keyusage(self): ca = self._create_ca() e = ca.x509.get_extension(1) self.assertEqual(e.get_short_name().decode(), 'keyUsage') self.assertEqual(e.get_critical(), True) self.assertEqual(e.get_data(), b'\x03\x02\x01\x06') def test_keyusage_not_critical(self): setattr(app_settings, 'CA_KEYUSAGE_CRITICAL', False) ca = self._create_ca() e = ca.x509.get_extension(1) self.assertEqual(e.get_short_name().decode(), 'keyUsage') self.assertEqual(e.get_critical(), False) setattr(app_settings, 'CA_KEYUSAGE_CRITICAL', True) def test_keyusage_value(self): setattr(app_settings, 'CA_KEYUSAGE_VALUE', 'cRLSign, keyCertSign, keyAgreement') ca = self._create_ca() e = ca.x509.get_extension(1) self.assertEqual(e.get_short_name().decode(), 'keyUsage') self.assertEqual(e.get_data(), b'\x03\x02\x01\x0e') setattr(app_settings, 'CA_KEYUSAGE_VALUE', 'cRLSign, keyCertSign') def test_subject_key_identifier(self): ca = self._create_ca() e = ca.x509.get_extension(2) self.assertEqual(e.get_short_name().decode(), 'subjectKeyIdentifier') self.assertEqual(e.get_critical(), False) e2 = crypto.X509Extension(b'subjectKeyIdentifier', False, b'hash', subject=ca.x509) self.assertEqual(e.get_data(), e2.get_data()) def test_authority_key_identifier(self): ca = self._create_ca() e = ca.x509.get_extension(3) self.assertEqual(e.get_short_name().decode(), 'authorityKeyIdentifier') self.assertEqual(e.get_critical(), False) e2 = crypto.X509Extension(b'authorityKeyIdentifier', False, b'keyid:always,issuer:always', issuer=ca.x509) self.assertEqual(e.get_data(), e2.get_data()) def test_extensions(self): extensions = [ { "name": "nsComment", "critical": False, "value": "CA - autogenerated Certificate" } ] ca = self._create_ca(extensions=extensions) e1 = ca.x509.get_extension(4) self.assertEqual(e1.get_short_name().decode(), 'nsComment') self.assertEqual(e1.get_critical(), False) self.assertEqual(e1.get_data(), b'\x16\x1eCA - autogenerated Certificate') def test_extensions_error1(self): extensions = {} try: self._create_ca(extensions=extensions) except ValidationError as e: # verify error message self.assertIn('Extension format invalid', str(e.message_dict['__all__'][0])) else: self.fail('ValidationError not raised') def test_extensions_error2(self): extensions = [{"wrong": "wrong"}] try: self._create_ca(extensions=extensions) except ValidationError as e: # verify error message self.assertIn('Extension format invalid', str(e.message_dict['__all__'][0])) else: self.fail('ValidationError not raised') def test_get_revoked_certs(self): ca = self._create_ca() c1 = self._create_cert(ca=ca) c2 = self._create_cert(ca=ca) c3 = self._create_cert(ca=ca) # noqa self.assertEqual(ca.get_revoked_certs().count(), 0) c1.revoke() self.assertEqual(ca.get_revoked_certs().count(), 1) c2.revoke() self.assertEqual(ca.get_revoked_certs().count(), 2) now = timezone.now() # expired certificates are not counted start = now - timedelta(days=6650) end = now - timedelta(days=6600) c4 = self._create_cert(ca=ca, validity_start=start, validity_end=end) c4.revoke() self.assertEqual(ca.get_revoked_certs().count(), 2) # inactive not counted yet start = now + timedelta(days=2) end = now + timedelta(days=365) c5 = self._create_cert(ca=ca, validity_start=start, validity_end=end) c5.revoke() self.assertEqual(ca.get_revoked_certs().count(), 2) def test_crl(self): ca, cert = self._prepare_revoked() crl = crypto.load_crl(crypto.FILETYPE_PEM, ca.crl) revoked_list = crl.get_revoked() self.assertIsNotNone(revoked_list) self.assertEqual(len(revoked_list), 1) self.assertEqual(int(revoked_list[0].get_serial()), cert.serial_number) def test_crl_view(self): ca, cert = self._prepare_revoked() response = self.client.get(reverse('x509:crl', args=[ca.pk])) self.assertEqual(response.status_code, 200) crl = crypto.load_crl(crypto.FILETYPE_PEM, response.content) revoked_list = crl.get_revoked() self.assertIsNotNone(revoked_list) self.assertEqual(len(revoked_list), 1) self.assertEqual(int(revoked_list[0].get_serial()), cert.serial_number) def test_crl_view_403(self): setattr(app_settings, 'CRL_PROTECTED', True) ca, cert = self._prepare_revoked() response = self.client.get(reverse('x509:crl', args=[ca.pk])) self.assertEqual(response.status_code, 403) setattr(app_settings, 'CRL_PROTECTED', False) def test_x509_text(self): ca = self._create_ca() text = crypto.dump_certificate(crypto.FILETYPE_TEXT, ca.x509) self.assertEqual(ca.x509_text, text.decode('utf-8')) def test_x509_import_exception_fixed(self): certificate = """-----BEGIN CERTIFICATE----- MIIEBTCCAu2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBRMQswCQYDVQQGEwJJVDEL MAkGA1UECAwCUk0xDTALBgNVBAcMBFJvbWExDzANBgNVBAoMBkNpbmVjYTEVMBMG A1UEAwwMUHJvdmEgQ2luZWNhMB4XDTE2MDkyMTA5MDQyOFoXDTM2MDkyMTA5MDQy OFowUTELMAkGA1UEBhMCSVQxCzAJBgNVBAgMAlJNMQ0wCwYDVQQHDARSb21hMQ8w DQYDVQQKDAZDaW5lY2ExFTATBgNVBAMMDFByb3ZhIENpbmVjYTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMV26pysBdm3OqhyyZjbWZ3ThmH6QTIDScTj +1y3nGgnIwgpHWJmZiO/XrwYburLttE+NP7qwgtRcVoxTJFnhuunSei8vE9lyooD l1wRUU0qMZSWB/Q3OF+S+FhRMtymx+H6a46yC5Wqxk0apNlvAJ1avuBtZjvipQHS Z3ub5iHpHr0LZKYbqq2yXna6SbGUjnGjVieIXTilbi/9yjukhNvoHC1fSXciV8hO 8GFuR5bUF/6kQFFMZsk3vXNTsKVx5ef7+zpN6n8lGmNAC8D28EqBxar4YAhuu8Jw +gvguEOji5BsF8pTu4NVBXia0xWjD1DKLmueVLu9rd4l2HGxsA0CAwEAAaOB5zCB 5DAMBgNVHRMEBTADAQH/MC0GCWCGSAGG+EIBDQQgFh5DQSAtIGF1dG9nZW5lcmF0 ZWQgQ2VydGlmaWNhdGUwCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQjUcBhP7i26o7R iaVbmRStMVsggTB5BgNVHSMEcjBwgBQjUcBhP7i26o7RiaVbmRStMVsggaFVpFMw UTELMAkGA1UEBhMCSVQxCzAJBgNVBAgMAlJNMQ0wCwYDVQQHDARSb21hMQ8wDQYD VQQKDAZDaW5lY2ExFTATBgNVBAMMDFByb3ZhIENpbmVjYYIBATANBgkqhkiG9w0B AQUFAAOCAQEAg0yQ8CGHGl4p2peALn63HxkAxKzxc8bD/bCItXHq3QFJAYRe5nuu eGBMdlVvlzh+N/xW1Jcl3+dg9UOlB5/eFr0BWXyk/0vtnJoMKjc4eVAcOlcbgk9s c0J4ZACrfjbBH9bU7OgYy4NwVXWQFbQqDZ4/beDnuA8JZcGV5+gK3H85pqGBndev 4DUTCrYk+kRLMyWLfurH7dSyw/9DXAmOVPB6SMkTK6sqkhwUmT6hEdADFUBTujes AjGrlOCMA8XDvvxVEl5nA6JjoPAQ8EIjYvxMykZE+nk0ZO4mqMG5DWCp/2ggodAD tnpHdm8yeMsoFPm+yZVDHDXjAirS6MX28w== -----END CERTIFICATE-----""" private_key = """-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAxXbqnKwF2bc6qHLJmNtZndOGYfpBMgNJxOP7XLecaCcjCCkd YmZmI79evBhu6su20T40/urCC1FxWjFMkWeG66dJ6Ly8T2XKigOXXBFRTSoxlJYH 9Dc4X5L4WFEy3KbH4fprjrILlarGTRqk2W8AnVq+4G1mO+KlAdJne5vmIekevQtk phuqrbJedrpJsZSOcaNWJ4hdOKVuL/3KO6SE2+gcLV9JdyJXyE7wYW5HltQX/qRA UUxmyTe9c1OwpXHl5/v7Ok3qfyUaY0ALwPbwSoHFqvhgCG67wnD6C+C4Q6OLkGwX ylO7g1UFeJrTFaMPUMoua55Uu72t3iXYcbGwDQIDAQABAoIBAD2pWa/c4+LNncqW Na++52gqcm9MB2nHrxSFoKueRoAboIve0uc0VLba/ok8E/7L6GXEyCXGRxvjrcLd XCyXqIET9zdvIFqmza11W6GLYtj20Q62Hvu69qaZrWVezcQrbIV7fnTL0mRFNLFF Ha8sQ4Pfn3VTlDYlGyPLgTcPQrjZlwD5OlzRNEbko/LkdNXZ3pvf4q17pjsxP3E7 XqD+d+dny+pBZL748Hp1RmNo/XfhF2Y4iIV4+3/CyBiTlnn8sURqQCeuoA42iCIH y28SBz0WS2FD/yVNbH0c4ZU+/R3Fwz5l7sHfaBieJeTFeqr5kuRU7Rro0EfFpa41 rT3fTz0CgYEA9/XpNsMtRLoMLqb01zvylgLO1cKNkAmoVFhAnh9nH1n3v55Vt48h K9NkHUPbVwSIVdQxDzQy+YXw9IEjieVCBOPHTxRHfX90Azup5dFVXznw6qs1GiW2 mXK+fLToVoTSCi9sHIbIkCAnKS7B5hzKxu+OicKKvouo7UM/NWiSGpsCgYEAy93i gN8leZPRSGXgS5COXOJ7zf8mqYWbzytnD5wh3XjWA2SNap93xyclCB7rlMfnOAXy 9rIgjrDEBBW7BwUyrYcB8M/qLvFfuf3rXgdhVzvA2OctdUdyzGERXObhiRopa2kq jFj4QyRa5kv7VTe85t9Ap2bqpE2nVD1wxRdaFncCgYBN0M+ijvfq5JQkI+MclMSZ jUIJ1WeFt3IrHhMRTHuZXCui5/awh2t6jHmTsZLpKRP8E35d7hy9L+qhYNGdWeQx Eqaey5dv7AqlZRj5dYtcOhvAGYCttv4qA9eB3Wg4lrAv4BgGj8nraRvBEdpp88kz S0SpOPM/vyaBZyQ0B6AqVwKBgQCvDvV03Cj94SSRGooj2RmmQQU2uqakYwqMNyTk jpm16BE+EJYuvIjKBp8R/hslQxMVVGZx2DuEy91F9LMJMDl4MLpF4wOhE7uzpor5 zzSTB8htePXcA2Jche227Ls2U7TFeyUCJ1Pns8wqfYxwfNBFH+gQ15sdQ2EwQSIY 3BiLuQKBgGG+yqKnBceb9zybnshSAVdGt933XjEwRUbaoXGnHjnCxsTtSGa0JkCT 2yrYrwM4KOr7LrKtvz703ApicJf+oRO+vW27+N5t0pyLCjsYJyL55RpM0KWJhKhT KQV8C/ciDV+lIw2yBmlCNvUmy7GAsHSZM+C8y29+GFR7an6WV+xa -----END RSA PRIVATE KEY-----""" ca = Ca(name='ImportTest error') ca.certificate = certificate ca.private_key = private_key ca.full_clean() ca.save() self.assertEqual(ca.email, '') def test_fill_subject_non_strings(self): ca1 = self._create_ca() ca2 = Ca(name='ca', organization_name=ca1) x509 = crypto.X509() subject = ca2._fill_subject(x509.get_subject()) self.assertEqual(subject.organizationName, 'Test CA') # this certificate has an invalid country code problematic_certificate = """-----BEGIN CERTIFICATE----- MIIEjzCCA3egAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQ8wDQYDVQQGEwZJdGFs aWExFjAUBgNVBAgMDUxhbWV6aWEgVGVybWUxFjAUBgNVBAcMDUxhbWV6aWEgVGVy bWUxIDAeBgNVBAoMF0NvbXVuZSBkaSBMYW1lemlhIFRlcm1lMRgwFgYDVQQDDA9M YW1lemlhZnJlZXdpZmkwHhcNMTIwMjE3MTQzMzAyWhcNMjIwMjE3MTQzMzAyWjB9 MQ8wDQYDVQQGEwZJdGFsaWExFjAUBgNVBAgMDUxhbWV6aWEgVGVybWUxFjAUBgNV BAcMDUxhbWV6aWEgVGVybWUxIDAeBgNVBAoMF0NvbXVuZSBkaSBMYW1lemlhIFRl cm1lMRgwFgYDVQQDDA9MYW1lemlhZnJlZXdpZmkwggEiMA0GCSqGSIb3DQEBAQUA A4IBDwAwggEKAoIBAQDBsEbRkpsgl9PZO+eb6M+2XDuENaDKIWxzEqhlQWqfivM5 SJNpIBij9n8vIgRu2ie7DmomBkU93tQWwL5EcZcSuqAnBgzkNmko5bsk9w7v6Apq V4UckIhtie7KRDCrG1XJaZ/0V4uYcW7+d1fYTCfMcgchpzMQsHAdjikyzRXc5TJn noV6eZf76zQGSaZllwl90VwQvEVe3VCKSja+zpYxsOjQgnKgrDx1O0l/RGxtCWGG fY9bizlD01nH4WuMT9ObO9F1YqnBc7pWtmRm4DfArr3yW5LKxkRrilwV1UCgQ80z yMYSeEIufChexzo1JBzrL7aEKnSm5fDvt3iJV3OlAgMBAAGjggEYMIIBFDAMBgNV HRMEBTADAQH/MC0GCWCGSAGG+EIBDQQgFh5DQSAtIGF1dG9nZW5lcmF0ZWQgQ2Vy dGlmaWNhdGUwCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBSsrs2asN5B2nSL36P72EBR MOLgijCBqAYDVR0jBIGgMIGdgBSsrs2asN5B2nSL36P72EBRMOLgiqGBgaR/MH0x DzANBgNVBAYTBkl0YWxpYTEWMBQGA1UECAwNTGFtZXppYSBUZXJtZTEWMBQGA1UE BwwNTGFtZXppYSBUZXJtZTEgMB4GA1UECgwXQ29tdW5lIGRpIExhbWV6aWEgVGVy bWUxGDAWBgNVBAMMD0xhbWV6aWFmcmVld2lmaYIBATANBgkqhkiG9w0BAQUFAAOC AQEAf6qG2iFfTv31bOWeE2GBO5VyT1l2MjB/waAXT4vPE2P3RVMoZguBZLc3hmbx nF6L5JlG7VbRqEE8wJMS5WeURuJe94CVftXJhzcd8ZnsISoGAh0IiRCLuTmpa/5q 3eWjgUwr3KldEJ77Sts72qSzRAD6C6RCMxnZTvcQzEjpomLLj1ID82lTrlrYl/in MDl+i5LuDRMlgj6PQhUgV+WoRESnZ/jL2MMxA/hcFPzfDDw6A2Kzgz4wzS5FMyHM iOCe57IN5gNeO2FAL351FHBONYQMtqeEEL82eSc53oFcLKCJf3E2yo1w6p5HB08H IuRFwXXuD2zUkZtldBcYeAa2oA== -----END CERTIFICATE-----""" problematic_private_key = """-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAwbBG0ZKbIJfT2Tvnm+jPtlw7hDWgyiFscxKoZUFqn4rzOUiT aSAYo/Z/LyIEbtonuw5qJgZFPd7UFsC+RHGXErqgJwYM5DZpKOW7JPcO7+gKaleF HJCIbYnuykQwqxtVyWmf9FeLmHFu/ndX2EwnzHIHIaczELBwHY4pMs0V3OUyZ56F enmX++s0BkmmZZcJfdFcELxFXt1Qiko2vs6WMbDo0IJyoKw8dTtJf0RsbQlhhn2P W4s5Q9NZx+FrjE/TmzvRdWKpwXO6VrZkZuA3wK698luSysZEa4pcFdVAoEPNM8jG EnhCLnwoXsc6NSQc6y+2hCp0puXw77d4iVdzpQIDAQABAoIBAQCvQLPjftbUV+x8 ++ImRTJkm/HSP7/8BOAfAvvRmq5CK7TF2TBgh4UkHq6X1BzUvJoEfBd5zmSqhcu7 xqyiO3FppemxRZ02hTEDq1J5MP6X/oomDIjJ/tEi5BJne+nZeMNXmjX8HZaW2dSH dS7L7KR6LZbcUXA4Ip1fcLlAWSb2Fe0bcuSLPaZZSmiA1Q3B/Q6nIOqPXDWq1/yz Vs7doSfniAt8CQse+NeWybevAHhaLjHIbqtvmAqmq91ehEiy87Cyj9VA5l4ggM8n O6DcmjSaiXfkLgJlrMQ50Ddxoqf35pf+vzebwFdYmyt3fGlIP1OaeVsfIGbkNFZG NQkdjEwhAoGBAObDqy8HMv070U+EXSdbv2x1A1glkA2ZUI1Ki+zXIrNV8ohZ4w71 /v2UsAAXxTCtx28EMFo923dHGk9OXM3EhmyNqYBRX97rB5V7Gt5FxmJs75punYaB IfMvo83Hn8mrBUUb74pQhhJ2TVVv/N3nefuElys6lMwyVgUBsu0xPt1pAoGBANbe qKouEl+lKdhfABbLCsgCp5yXhFEgNMuGArj5Op/mw/RWOYs4TuN35WmzpmsQZ2PD +cr+/oN+eJ7zgyStDJmMkeG4vtUVJ5F4wWFWgwgY7zU1J3tu0e/EvgaaLkqWtLRE xGJ0zc0qHQdOGGxnQPUy49yvMsdrVwHT/RQiJdDdAoGAAnxlIbKQKA426QZiAoSI gWCZUp/E94CJT5xX+YsvwoLQhAuD2Ktpvc2WP8oBw857cYS4CKDV9mj7rZMIiObv E8hK5Sj7QWmCwWd8GJzj0DegNSev5r0JYpdGyna2D/QZsG7mm7TWXOiNWLhGHxXZ SI5bGoodBD4ekxs7lDaNmNECgYEAoVVd3ynosdgZq1TphDPATJ1xrKo3t5IvEgH1 WV4JHrbuuy9i1Z3Z3gHQR6WUdx9CAi7MCBeekq0LdI3zEj69Dy30+z70Spovs5Kv 4J5MlG/kbFcU5iE3kIhxBhQOXgL6e8CGlEaPoFTWpv2EaSC+LV2gqbsCralzEvRR OiTJsCECgYEAzdFUEea4M6Uavsd36mBbCLAYkYvhMMYUcrebFpDFwZUFaOrNV0ju 5YkQTn0EQuwQWKcfs+Z+HRiqMmqj5RdgxQs6pCQG9nfp0uVSflZATOiweshGjn6f wZWuZRQLPPTAdiW+drs3gz8w0u3Y9ihgvHQqFcGJ1+j6ANJ0XdE/D5Y= -----END RSA PRIVATE KEY-----""" def test_ca_invalid_country(self): ca = self._create_ca(name='ImportTest error', certificate=self.problematic_certificate, private_key=self.problematic_private_key) self.assertEqual(ca.country_code, '') def test_import_ca_cert_validation_error(self): certificate = self.import_certificate[20:] private_key = self.import_private_key ca = Ca(name="TestCaCertValidation") try: ca.certificate = certificate ca.private_key = private_key ca.full_clean() except ValidationError as e: self.assertIn("[('PEM routines', 'PEM_read_bio', 'no start line')]", str(e.message_dict['certificate'][0])) else: self.fail('ValidationError not raised') def test_import_ca_key_validation_error(self): certificate = self.import_certificate private_key = self.import_private_key[20:] ca = Ca(name="TestCaKeyValidation") try: ca.certificate = certificate ca.private_key = private_key ca.full_clean() ca.save() except ValidationError as e: self.assertIn("[('PEM routines', 'PEM_read_bio', 'no start line')]", str(e.message_dict['private_key'][0])) else: self.fail('ValidationError not raised') def test_create_old_serial_ca(self): ca = self._create_ca(serial_number=3) self.assertEqual(int(ca.serial_number), 3) cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca.certificate) self.assertEqual(int(cert.get_serial_number()), int(ca.serial_number)) def test_bad_serial_number_ca(self): try: self._create_ca(serial_number='notIntegers') except ValidationError as e: self.assertEqual("Serial number must be an integer", str(e.message_dict['serial_number'][0])) django-x509-0.4.1/django_x509/tests/test_cert.py0000664000175000017500000004245113343027425022363 0ustar nemesisnemesis00000000000000from datetime import datetime, timedelta from django.core.exceptions import ValidationError from django.test import TestCase from django.utils import timezone from OpenSSL import crypto from . import TestX509Mixin from .. import settings as app_settings from ..base.models import generalized_time from ..models import Ca, Cert class TestCert(TestX509Mixin, TestCase): """ tests for Cert model """ ca_model = Ca cert_model = Cert import_certificate = """ -----BEGIN CERTIFICATE----- MIICMTCCAdugAwIBAgIDAeJAMA0GCSqGSIb3DQEBBQUAMGgxETAPBgNVBAoMCE9w ZW5XSVNQMQswCQYDVQQGEwJJVDEMMAoGA1UEAwwDb3cyMQ0wCwYDVQQHDARSb21l MRwwGgYJKoZIhvcNAQkBFg10ZXN0QHRlc3QuY29tMQswCQYDVQQIDAJSTTAiGA8y MDE1MTEwMTAwMDAwMFoYDzIxMTgxMTAyMTgwMDI1WjAAMFwwDQYJKoZIhvcNAQEB BQADSwAwSAJBANh0Y7oG5JUl9cCBs6E11cJ2xLul6zw8cEoD1L7NazrPXG/NGTLt OF2TOEUob24aQ+YagMD6HLbejV0baTXwXakCAwEAAaOB0TCBzjAJBgNVHRMEAjAA MAsGA1UdDwQEAwIFoDAdBgNVHQ4EFgQUpcvUDhxzJFpMvjlTQjBaCjQI/3QwgZQG A1UdIwSBjDCBiYAUwfnP0B5rF3xo7yDRAda+1nj6QqahbKRqMGgxETAPBgNVBAoM CE9wZW5XSVNQMQswCQYDVQQGEwJJVDEMMAoGA1UEAwwDb3cyMQ0wCwYDVQQHDARS b21lMRwwGgYJKoZIhvcNAQkBFg10ZXN0QHRlc3QuY29tMQswCQYDVQQIDAJSTYID AeJAMA0GCSqGSIb3DQEBBQUAA0EAUKog+BPsM8j34Clec2BAACcuyJlwX41vQ3kG FqQS2KfO7YIk5ITWhX8y0P//u+ENWRlnVTRQma9d5tYYJvL8+Q== -----END CERTIFICATE----- """ import_private_key = """ -----BEGIN PRIVATE KEY----- MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2HRjugbklSX1wIGz oTXVwnbEu6XrPDxwSgPUvs1rOs9cb80ZMu04XZM4RShvbhpD5hqAwPoctt6NXRtp NfBdqQIDAQABAkEAx9M7NcOjRqXSqDOU92DRxEMNAAb+kY9iQpIi1zqgoZqWduVK tq0X0ous54j2ItqKDHxqEbbBzlo/BxMn5zkdOQIhAPIlngBgjgM0FFt+4bw6+5mW VvjxIQoVHkmd1HsfHkPvAiEA5NZ+Zqbbv6T7oLgixye1nbcJ3mQ5+IUuamGp7dVq /+cCIQDpxVNCffTcNt0ob9gyRqc74Z5Ze0EwYK761zqZGrO3VQIgYp0UZ4QsWo/s Z7wyMISqPUbtl8q1OKWb9PgVVIqNy60CIEpi865urZNSIz4SRrxn4r+WV9Mxlfxs 1xtxYxSjiqrj -----END PRIVATE KEY----- """ import_ca_certificate = """ -----BEGIN CERTIFICATE----- MIICpTCCAk+gAwIBAgIDAeJAMA0GCSqGSIb3DQEBBQUAMGgxETAPBgNVBAoMCE9w ZW5XSVNQMQswCQYDVQQGEwJJVDEMMAoGA1UEAwwDb3cyMQ0wCwYDVQQHDARSb21l MRwwGgYJKoZIhvcNAQkBFg10ZXN0QHRlc3QuY29tMQswCQYDVQQIDAJSTTAiGA8y MDE1MTEwMTAwMDAwMFoYDzIxMjcxMDMxMTc1OTI1WjBoMREwDwYDVQQKDAhPcGVu V0lTUDELMAkGA1UEBhMCSVQxDDAKBgNVBAMMA293MjENMAsGA1UEBwwEUm9tZTEc MBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTELMAkGA1UECAwCUk0wXDANBgkq hkiG9w0BAQEFAANLADBIAkEAsz5ORGAkryOe3bHRsuBJjCbwvPh4peSfpdrRV9CS iz7HQWq1s+wdzHONvc8pin+lmnB+RhGm0LrZDOWRyfzjMwIDAQABo4HdMIHaMBIG A1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTB+c/Q HmsXfGjvINEB1r7WePpCpjCBlAYDVR0jBIGMMIGJgBTB+c/QHmsXfGjvINEB1r7W ePpCpqFspGowaDERMA8GA1UECgwIT3BlbldJU1AxCzAJBgNVBAYTAklUMQwwCgYD VQQDDANvdzIxDTALBgNVBAcMBFJvbWUxHDAaBgkqhkiG9w0BCQEWDXRlc3RAdGVz dC5jb20xCzAJBgNVBAgMAlJNggMB4kAwDQYJKoZIhvcNAQEFBQADQQAeHppFPgUx TPJ0Vv9oZHcaOTww6S2p/X/F6yCHZMYq83B+cVxcJ4v+MVxRLg7DBVAIA8gOEFy2 sKMLWX3IKJmh -----END CERTIFICATE----- """ import_ca_private_key = """ -----BEGIN PRIVATE KEY----- MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAsz5ORGAkryOe3bHR suBJjCbwvPh4peSfpdrRV9CSiz7HQWq1s+wdzHONvc8pin+lmnB+RhGm0LrZDOWR yfzjMwIDAQABAkEAnG5ICEyQN3my8HB8PsyX44UonQOM59s7qZfrE+SnwHU2ywhE k9Y1S1C9VB0YsDZTeZUggJNSDN4YrKjIevYZQQIhAOWec6vngM/PlI1adrFndd3d 2WlyfnXwE/RFzVDOfOcrAiEAx9Y1ZbtTr2AL6wsf+wpRbkq9dPEiWi4C+0ms3Uw2 8BkCIGRctohLnqS2QWLrSHfQFdeM0StizN11uvMI023fYv6TAiEAxujn85/3V1wh 4M4NAiMuFLseQ5V1XQ/pddjK0Od405kCIC2ezclTgDBbRkHXKFtKnoj3/pGUsa3K 5XIa5rp5Is47 -----END PRIVATE KEY----- """ def test_new(self): cert = self._create_cert() self.assertNotEqual(cert.certificate, '') self.assertNotEqual(cert.private_key, '') x509 = cert.x509 self.assertEqual(x509.get_serial_number(), cert.serial_number) subject = x509.get_subject() # check subject self.assertEqual(subject.countryName, cert.country_code) self.assertEqual(subject.stateOrProvinceName, cert.state) self.assertEqual(subject.localityName, cert.city) self.assertEqual(subject.organizationName, cert.organization_name) self.assertEqual(subject.emailAddress, cert.email) self.assertEqual(subject.commonName, cert.common_name) # check issuer issuer = x509.get_issuer() ca = cert.ca self.assertEqual(issuer.countryName, ca.country_code) self.assertEqual(issuer.stateOrProvinceName, ca.state) self.assertEqual(issuer.localityName, ca.city) self.assertEqual(issuer.organizationName, ca.organization_name) self.assertEqual(issuer.emailAddress, ca.email) self.assertEqual(issuer.commonName, ca.common_name) # check signature store = crypto.X509Store() store.add_cert(ca.x509) store_ctx = crypto.X509StoreContext(store, cert.x509) store_ctx.verify_certificate() # ensure version is 3 (indexed 0 based counting) self.assertEqual(x509.get_version(), 2) # basic constraints e = cert.x509.get_extension(0) self.assertEqual(e.get_critical(), 0) self.assertEqual(e.get_short_name().decode(), 'basicConstraints') self.assertEqual(e.get_data(), b'0\x00') def test_x509_property(self): cert = self._create_cert() x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert.certificate) self.assertEqual(cert.x509.get_subject(), x509.get_subject()) self.assertEqual(cert.x509.get_issuer(), x509.get_issuer()) def test_x509_property_none(self): self.assertIsNone(Cert().x509) def test_pkey_property(self): cert = self._create_cert() self.assertIsInstance(cert.pkey, crypto.PKey) def test_pkey_property_none(self): self.assertIsNone(Cert().pkey) def test_default_validity_end(self): cert = Cert() self.assertEqual(cert.validity_end.year, datetime.now().year + 1) def test_default_validity_start(self): cert = Cert() expected = datetime.now() - timedelta(days=1) self.assertEqual(cert.validity_start.year, expected.year) self.assertEqual(cert.validity_start.month, expected.month) self.assertEqual(cert.validity_start.day, expected.day) self.assertEqual(cert.validity_start.hour, 0) self.assertEqual(cert.validity_start.minute, 0) self.assertEqual(cert.validity_start.second, 0) def test_import_cert(self): ca = Ca(name='ImportTest') ca.certificate = self.import_ca_certificate ca.private_key = self.import_ca_private_key ca.full_clean() ca.save() cert = Cert(name='ImportCertTest', ca=ca, certificate=self.import_certificate, private_key=self.import_private_key) cert.full_clean() cert.save() x509 = cert.x509 # verify attributes self.assertEqual(int(x509.get_serial_number()), 123456) subject = x509.get_subject() self.assertEqual(subject.countryName, None) self.assertEqual(subject.stateOrProvinceName, None) self.assertEqual(subject.localityName, None) self.assertEqual(subject.organizationName, None) self.assertEqual(subject.emailAddress, None) self.assertEqual(subject.commonName, None) issuer = x509.get_issuer() self.assertEqual(issuer.countryName, 'IT') self.assertEqual(issuer.stateOrProvinceName, 'RM') self.assertEqual(issuer.localityName, 'Rome') self.assertEqual(issuer.organizationName, 'OpenWISP') self.assertEqual(issuer.emailAddress, 'test@test.com') self.assertEqual(issuer.commonName, 'ow2') # verify field attribtues self.assertEqual(cert.key_length, '512') self.assertEqual(cert.digest, 'sha1') start = timezone.make_aware(datetime.strptime('20151101000000Z', generalized_time)) self.assertEqual(cert.validity_start, start) end = timezone.make_aware(datetime.strptime('21181102180025Z', generalized_time)) self.assertEqual(cert.validity_end, end) self.assertEqual(cert.country_code, '') self.assertEqual(cert.state, '') self.assertEqual(cert.city, '') self.assertEqual(cert.organization_name, '') self.assertEqual(cert.email, '') self.assertEqual(cert.common_name, '') self.assertEqual(int(cert.serial_number), 123456) # ensure version is 3 (indexed 0 based counting) self.assertEqual(x509.get_version(), 2) cert.delete() # test auto name cert = Cert(certificate=self.import_certificate, private_key=self.import_private_key, ca=ca) cert.full_clean() cert.save() self.assertEqual(cert.name, '123456') def test_import_private_key_empty(self): ca = Ca(name='ImportTest') ca.certificate = self.import_ca_certificate ca.private_key = self.import_ca_private_key ca.full_clean() ca.save() cert = Cert(name='ImportTest', ca=ca) cert.certificate = self.import_certificate try: cert.full_clean() except ValidationError as e: # verify error message self.assertIn('importing an existing certificate', str(e)) else: self.fail('ValidationError not raised') def test_import_wrong_ca(self): # test auto name cert = Cert(certificate=self.import_certificate, private_key=self.import_private_key, ca=self._create_ca()) try: cert.full_clean() except ValidationError as e: # verify error message self.assertIn('CA doesn\'t match', str(e.message_dict['__all__'][0])) else: self.fail('ValidationError not raised') def test_keyusage(self): cert = self._create_cert() e = cert.x509.get_extension(1) self.assertEqual(e.get_short_name().decode(), 'keyUsage') self.assertEqual(e.get_critical(), False) self.assertEqual(e.get_data(), b'\x03\x02\x05\xa0') def test_keyusage_critical(self): setattr(app_settings, 'CERT_KEYUSAGE_CRITICAL', True) cert = self._create_cert() e = cert.x509.get_extension(1) self.assertEqual(e.get_short_name().decode(), 'keyUsage') self.assertEqual(e.get_critical(), True) setattr(app_settings, 'CERT_KEYUSAGE_CRITICAL', False) def test_keyusage_value(self): setattr(app_settings, 'CERT_KEYUSAGE_VALUE', 'digitalSignature') cert = self._create_cert() e = cert.x509.get_extension(1) self.assertEqual(e.get_short_name().decode(), 'keyUsage') self.assertEqual(e.get_data(), b'\x03\x02\x07\x80') setattr(app_settings, 'CERT_KEYUSAGE_VALUE', 'digitalSignature, keyEncipherment') def test_subject_key_identifier(self): cert = self._create_cert() e = cert.x509.get_extension(2) self.assertEqual(e.get_short_name().decode(), 'subjectKeyIdentifier') self.assertEqual(e.get_critical(), False) e2 = crypto.X509Extension(b'subjectKeyIdentifier', False, b'hash', subject=cert.x509) self.assertEqual(e.get_data(), e2.get_data()) def test_authority_key_identifier(self): cert = self._create_cert() e = cert.x509.get_extension(3) self.assertEqual(e.get_short_name().decode(), 'authorityKeyIdentifier') self.assertEqual(e.get_critical(), False) e2 = crypto.X509Extension(b'authorityKeyIdentifier', False, b'keyid:always,issuer:always', issuer=cert.ca.x509) self.assertEqual(e.get_data(), e2.get_data()) def test_extensions(self): extensions = [ { "name": "nsCertType", "critical": False, "value": "client" }, { "name": "extendedKeyUsage", "critical": True, # critical just for testing purposes "value": "clientAuth" } ] cert = self._create_cert(extensions=extensions) e1 = cert.x509.get_extension(4) self.assertEqual(e1.get_short_name().decode(), 'nsCertType') self.assertEqual(e1.get_critical(), False) self.assertEqual(e1.get_data(), b'\x03\x02\x07\x80') e2 = cert.x509.get_extension(5) self.assertEqual(e2.get_short_name().decode(), 'extendedKeyUsage') self.assertEqual(e2.get_critical(), True) self.assertEqual(e2.get_data(), b'0\n\x06\x08+\x06\x01\x05\x05\x07\x03\x02') def test_extensions_error1(self): extensions = {} try: self._create_cert(extensions=extensions) except ValidationError as e: # verify error message self.assertIn('Extension format invalid', str(e.message_dict['__all__'][0])) else: self.fail('ValidationError not raised') def test_extensions_error2(self): extensions = [ {"wrong": "wrong"} ] try: self._create_cert(extensions=extensions) except ValidationError as e: # verify error message self.assertIn('Extension format invalid', str(e.message_dict['__all__'][0])) else: self.fail('ValidationError not raised') def test_revoke(self): cert = self._create_cert() self.assertFalse(cert.revoked) self.assertIsNone(cert.revoked_at) cert.revoke() self.assertTrue(cert.revoked) self.assertIsNotNone(cert.revoked_at) def test_x509_text(self): cert = self._create_cert() text = crypto.dump_certificate(crypto.FILETYPE_TEXT, cert.x509) self.assertEqual(cert.x509_text, text.decode('utf-8')) def test_fill_subject_None_attrs(self): # ensure no exception raised if model attrs are set to None x509 = crypto.X509() cert = Cert(name='test', ca=self._create_ca()) cert._fill_subject(x509.get_subject()) self.country_code = 'IT' cert._fill_subject(x509.get_subject()) self.state = 'RM' cert._fill_subject(x509.get_subject()) self.city = 'Rome' cert._fill_subject(x509.get_subject()) self.organization_name = 'OpenWISP' cert._fill_subject(x509.get_subject()) self.email = 'test@test.com' cert._fill_subject(x509.get_subject()) def test_cert_create(self): ca = Ca(name='Test CA') ca.full_clean() ca.save() Cert.objects.create( ca=ca, common_name='TestCert1', name='TestCert1', ) def test_import_cert_validation_error(self): certificate = self.import_certificate[20:] private_key = self.import_private_key ca = Ca(name='TestImportCertValidation') ca.certificate = self.import_ca_certificate ca.private_key = self.import_ca_private_key ca.full_clean() ca.save() try: cert = Cert(name='TestCertValidation', ca=ca, certificate=certificate, private_key=private_key) cert.full_clean() except ValidationError as e: self.assertIn("[('PEM routines', 'PEM_read_bio', 'no start line')]", str(e.message_dict['certificate'][0])) else: self.fail('ValidationError not raised') def test_import_key_validation_error(self): certificate = self.import_certificate private_key = self.import_private_key[20:] ca = Ca(name='TestImportKeyValidation') ca.certificate = self.import_certificate ca.private_key = self.import_private_key ca.full_clean() ca.save() try: cert = Cert(name='TestKeyValidation', ca=ca, certificate=certificate, private_key=private_key) cert.full_clean() except ValidationError as e: self.assertIn("[('PEM routines', 'PEM_read_bio', 'no start line')]", str(e.message_dict['private_key'][0])) else: self.fail('ValidationError not raised') def test_create_old_serial_certificate(self): cert = self._create_cert(serial_number=3) self.assertEqual(int(cert.serial_number), 3) x509 = cert.x509 self.assertEqual(int(x509.get_serial_number()), 3) def test_bad_serial_number_cert(self): try: self._create_cert(serial_number='notIntegers') except ValidationError as e: self.assertEqual("Serial number must be an integer", str(e.message_dict['serial_number'][0])) def test_serial_number_clash(self): ca = Ca(name='TestSerialClash') ca.certificate = self.import_ca_certificate ca.private_key = self.import_ca_private_key ca.save() cert = self._create_cert(serial_number=123456, ca=ca) cert.full_clean() cert.save() _cert = Cert(name='TestClash', ca=ca, certificate=self.import_certificate, private_key=self.import_private_key) try: _cert.full_clean() except ValidationError as e: self.assertEqual("Certificate with this CA and Serial number already exists.", str(e.message_dict['__all__'][0])) django-x509-0.4.1/django_x509/tests/test_helpers.py0000664000175000017500000000072213234215531023057 0ustar nemesisnemesis00000000000000from django.contrib.messages.storage.fallback import FallbackStorage from django.http import HttpRequest class MessagingRequest(HttpRequest): session = 'session' def __init__(self): super(MessagingRequest, self).__init__() self._messages = FallbackStorage(self) def get_messages(self): return getattr(self._messages, '_queued_messages') def get_message_strings(self): return [str(m) for m in self.get_messages()] django-x509-0.4.1/django_x509/templates/0000775000175000017500000000000013343702155020643 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/templates/admin/0000775000175000017500000000000013343702155021733 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/templates/admin/django_x509/0000775000175000017500000000000013343702155023762 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/templates/admin/django_x509/change_form.html0000664000175000017500000000104413177564257027135 0ustar nemesisnemesis00000000000000{% extends "admin/change_form.html" %} {% load i18n %} {% block object-tools-items %} {% if opts.model_name == 'ca' %}
  • {% trans "Download CRL" %}
  • {% endif %} {{ block.super }} {% endblock %} {% block after_field_sets %} {% if not add %}
    {{ original.x509_text }}
    {% endif %} {% endblock %} django-x509-0.4.1/django_x509/admin.py0000664000175000017500000000043413234215531020304 0ustar nemesisnemesis00000000000000from django.contrib import admin from .base.admin import AbstractCaAdmin, AbstractCertAdmin from .models import Ca, Cert class CertAdmin(AbstractCertAdmin): pass class CaAdmin(AbstractCaAdmin): pass admin.site.register(Ca, CaAdmin) admin.site.register(Cert, CertAdmin) django-x509-0.4.1/django_x509/settings.py0000664000175000017500000000201113177564257021067 0ustar nemesisnemesis00000000000000from django.conf import settings DEFAULT_CERT_VALIDITY = getattr(settings, 'DJANGO_X509_DEFAULT_CERT_VALIDITY', 365) DEFAULT_CA_VALIDITY = getattr(settings, 'DJANGO_X509_DEFAULT_CA_VALIDITY', 3650) DEFAULT_KEY_LENGTH = str(getattr(settings, 'DJANGO_X509_DEFAULT_KEY_LENGTH', '2048')) DEFAULT_DIGEST_ALGORITHM = getattr(settings, 'DJANGO_X509_DEFAULT_DIGEST_ALGORITHM', 'sha256') CA_BASIC_CONSTRAINTS_CRITICAL = getattr(settings, 'DJANGO_X509_CA_BASIC_CONSTRAINTS_CRITICAL', True) CA_BASIC_CONSTRAINTS_PATHLEN = getattr(settings, 'DJANGO_X509_CA_BASIC_CONSTRAINTS_PATHLEN', 0) CA_KEYUSAGE_CRITICAL = getattr(settings, 'DJANGO_X509_CA_KEYUSAGE_CRITICAL', True) CA_KEYUSAGE_VALUE = getattr(settings, 'DJANGO_X509_CA_KEYUSAGE_VALUE', 'cRLSign, keyCertSign') CERT_KEYUSAGE_CRITICAL = getattr(settings, 'DJANGO_X509_CERT_KEYUSAGE_CRITICAL', False) CERT_KEYUSAGE_VALUE = getattr(settings, 'DJANGO_X509_CERT_KEYUSAGE_VALUE', 'digitalSignature, keyEncipherment') # noqa CRL_PROTECTED = getattr(settings, 'DJANGO_X509_CRL_PROTECTED', False) django-x509-0.4.1/django_x509/static/0000775000175000017500000000000013343702155020134 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/static/django-x509/0000775000175000017500000000000013343702155022101 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/static/django-x509/js/0000775000175000017500000000000013343702155022515 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/static/django-x509/js/x509-admin.js0000664000175000017500000000347113343026455024655 0ustar nemesisnemesis00000000000000django.jQuery(function ($) { 'use strict'; // select private_key/certificate field text on click $('.field-certificate, .field-private_key').find(".readonly").on('click', function () { var range, selection; if (window.getSelection) { selection = window.getSelection(); range = document.createRange(); range.selectNodeContents(this); selection.removeAllRanges(); selection.addRange(range); } else if (document.body.createTextRange) { range = document.body.createTextRange(); range.moveToElementText(this); range.select(); } }); var operationType = $('.field-operation_type select'); // enable switcher only in add forms if (!operationType.length || $('form .deletelink-box').length > 0) { $('.field-operation_type').hide(); return; } // function for operation_type switcher var showFields = function () { // define fields for each operation var importFields = $('.form-row:not(.field-certificate, .field-operation_type, ' + '.field-private_key, .field-name, .field-ca)'), newFields = $('.form-row:not(.field-certificate, .field-private_key)'), defaultFields = $('.form-row:not(.field-operation_type)'), allFields = $('.form-row'), value = operationType.val(); if (value === '-') { allFields.show(); defaultFields.hide(); } if (value === 'new') { allFields.hide(); newFields.show(); } if (value === 'import') { allFields.show(); importFields.hide(); } }; showFields(); operationType.on('change', function (e) { showFields(); }); }); django-x509-0.4.1/django_x509/static/django-x509/css/0000775000175000017500000000000013343702155022671 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/static/django-x509/css/admin.css0000664000175000017500000000107613177564257024515 0ustar nemesisnemesis00000000000000.field-certificate p, .field-certificate div.readonly, .field-certificate .vLargeTextField, .field-private_key p, .field-private_key div.readonly, .field-private_key .vLargeTextField, pre.x509{ font-family: monospace; white-space: pre; } .field-certificate .vLargeTextField, .field-private_key .vLargeTextField{ height: 350px; } .field-certificate p.help, .field-private_key p.help{ font-family: inherit; white-space: normal; } fieldset.x509-custom { margin-top: 0 } fieldset.x509-custom .form-row{ padding: 0 } fieldset.x509-custom pre{ margin-top: 0 } django-x509-0.4.1/django_x509/utils.py0000664000175000017500000000041413177564257020374 0ustar nemesisnemesis00000000000000import sys import six def bytes_compat(string, encoding='utf8'): if sys.version_info.major >= 3: if not isinstance(string, six.string_types): string = str(string) return bytes(string, encoding) else: return bytes(string) django-x509-0.4.1/django_x509/__init__.py0000664000175000017500000000070613343701321020753 0ustar nemesisnemesis00000000000000VERSION = (0, 4, 1, 'final') __version__ = VERSION # alias def get_version(): version = '%s.%s' % (VERSION[0], VERSION[1]) if VERSION[2]: version = '%s.%s' % (version, VERSION[2]) if VERSION[3:] == ('alpha', 0): version = '%s pre-alpha' % version else: if VERSION[3] != 'final': version = '%s %s' % (version, VERSION[3]) return version default_app_config = 'django_x509.apps.DjangoX509Config' django-x509-0.4.1/django_x509/urls.py0000664000175000017500000000023313234215531020176 0ustar nemesisnemesis00000000000000from django.conf.urls import url from . import views app_name = 'x509' urlpatterns = [ url(r'^x509/ca/(?P[^/]+).crl$', views.crl, name='crl'), ] django-x509-0.4.1/django_x509/apps.py0000664000175000017500000000030213177564257020173 0ustar nemesisnemesis00000000000000from django.apps import AppConfig from django.utils.translation import ugettext_lazy as _ class DjangoX509Config(AppConfig): name = 'django_x509' verbose_name = _('x509 Certificates') django-x509-0.4.1/django_x509/base/0000775000175000017500000000000013343702155017557 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/base/models.py0000664000175000017500000004361113343026455021423 0ustar nemesisnemesis00000000000000import collections import uuid from datetime import datetime, timedelta import OpenSSL from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from jsonfield import JSONField from model_utils.fields import AutoCreatedField, AutoLastModifiedField from OpenSSL import crypto from six import string_types from .. import settings as app_settings from ..utils import bytes_compat generalized_time = '%Y%m%d%H%M%SZ' KEY_LENGTH_CHOICES = ( ('', ''), ('512', '512'), ('1024', '1024'), ('2048', '2048'), ('4096', '4096') ) DIGEST_CHOICES = ( ('', ''), ('sha1', 'SHA1'), ('sha224', 'SHA224'), ('sha256', 'SHA256'), ('sha384', 'SHA384'), ('sha512', 'SHA512'), ) SIGNATURE_MAPPING = { 'sha1WithRSAEncryption': 'sha1', 'sha224WithRSAEncryption': 'sha224', 'sha256WithRSAEncryption': 'sha256', 'sha384WithRSAEncryption': 'sha384', 'sha512WithRSAEncryption': 'sha512', } def default_validity_start(): """ Sets validity_start field to 1 day before the current date (avoids "certificate not valid yet" edge case). In some cases, because of timezone differences, when certificates were just created they were considered valid in a timezone (eg: Europe) but not yet valid in another timezone (eg: US). This function intentionally returns naive datetime (not timezone aware), so that certificates are valid from 00:00 AM in all timezones. """ start = datetime.now() - timedelta(days=1) return start.replace(hour=0, minute=0, second=0, microsecond=0) def default_ca_validity_end(): """ returns the default value for validity_end field """ delta = timedelta(days=app_settings.DEFAULT_CA_VALIDITY) return timezone.now() + delta def default_cert_validity_end(): """ returns the default value for validity_end field """ delta = timedelta(days=app_settings.DEFAULT_CERT_VALIDITY) return timezone.now() + delta def default_key_length(): """ returns default value for key_length field (this avoids to set the exact default value in the database migration) """ return app_settings.DEFAULT_KEY_LENGTH def default_digest_algorithm(): """ returns default value for digest field (this avoids to set the exact default value in the database migration) """ return app_settings.DEFAULT_DIGEST_ALGORITHM @python_2_unicode_compatible class BaseX509(models.Model): """ Abstract Cert class, shared between Ca and Cert """ name = models.CharField(max_length=64) notes = models.TextField(blank=True) key_length = models.CharField(_('key length'), help_text=_('bits'), blank=True, choices=KEY_LENGTH_CHOICES, default=default_key_length, max_length=6) digest = models.CharField(_('digest algorithm'), help_text=_('bits'), blank=True, choices=DIGEST_CHOICES, default=default_digest_algorithm, max_length=8) validity_start = models.DateTimeField(blank=True, null=True, default=default_validity_start) validity_end = models.DateTimeField(blank=True, null=True, default=default_cert_validity_end) country_code = models.CharField(max_length=2, blank=True) state = models.CharField(_('state or province'), max_length=64, blank=True) city = models.CharField(_('city'), max_length=64, blank=True) organization_name = models.CharField(_('organization'), max_length=64, blank=True) organizational_unit_name = models.CharField(_('organizational unit name'), max_length=64, blank=True) email = models.EmailField(_('email address'), blank=True) common_name = models.CharField(_('common name'), max_length=63, blank=True) extensions = JSONField(_('extensions'), default=list, blank=True, help_text=_('additional x509 certificate extensions'), load_kwargs={'object_pairs_hook': collections.OrderedDict}, dump_kwargs={'indent': 4}) # serial_number is set to CharField as a UUID integer is too big for a # PositiveIntegerField and an IntegerField on SQLite serial_number = models.CharField(_('serial number'), help_text=_('leave blank to determine automatically'), blank=True, null=True, max_length=39) certificate = models.TextField(blank=True, help_text='certificate in X.509 PEM format') private_key = models.TextField(blank=True, help_text='private key in X.509 PEM format') created = AutoCreatedField(_('created'), editable=True) modified = AutoLastModifiedField(_('modified'), editable=True) class Meta: abstract = True def __str__(self): return self.name def clean_fields(self, *args, **kwargs): # importing existing certificate # must be done here in order to validate imported fields # and fill private and public key before validation fails if self._state.adding and self.certificate and self.private_key: self._validate_pem() self._import() super(BaseX509, self).clean_fields(*args, **kwargs) def clean(self): # when importing, both public and private must be present if ( (self.certificate and not self.private_key) or (self.private_key and not self.certificate) ): raise ValidationError(_('When importing an existing certificate, both' 'keys (private and public) must be present')) if self.serial_number: self._validate_serial_number() self._verify_extension_format() def save(self, *args, **kwargs): generate = False if not self.id and not self.certificate and not self.private_key: generate = True super(BaseX509, self).save(*args, **kwargs) if generate: # automatically determine serial number if not self.serial_number: self.serial_number = uuid.uuid4().int self._generate() kwargs['force_insert'] = False super(BaseX509, self).save(*args, **kwargs) @cached_property def x509(self): """ returns an instance of OpenSSL.crypto.X509 """ if self.certificate: return crypto.load_certificate(crypto.FILETYPE_PEM, self.certificate) @cached_property def x509_text(self): """ returns a text dump of the information contained in the x509 certificate """ if self.certificate: text = crypto.dump_certificate(crypto.FILETYPE_TEXT, self.x509) return text.decode('utf-8') @cached_property def pkey(self): """ returns an instance of OpenSSL.crypto.PKey """ if self.private_key: return crypto.load_privatekey(crypto.FILETYPE_PEM, self.private_key) def _validate_pem(self): """ (internal use only) validates certificate and private key """ errors = {} for field in ['certificate', 'private_key']: method_name = 'load_{0}'.format(field.replace('_', '')) load_pem = getattr(crypto, method_name) try: load_pem(crypto.FILETYPE_PEM, getattr(self, field)) except OpenSSL.crypto.Error as e: errors[field] = ValidationError(_('OpenSSL error: {0}'.format(e.args[0]))) if errors: raise ValidationError(errors) def _validate_serial_number(self): """ (internal use only) validates serial number if set manually """ try: int(self.serial_number) except ValueError: raise ValidationError({'serial_number': _('Serial number must be an integer')}) def _generate(self): """ (internal use only) generates a new x509 certificate (CA or end-entity) """ key = crypto.PKey() key.generate_key(crypto.TYPE_RSA, int(self.key_length)) cert = crypto.X509() subject = self._fill_subject(cert.get_subject()) cert.set_version(0x2) # version 3 (0 indexed counting) cert.set_subject(subject) cert.set_serial_number(int(self.serial_number)) cert.set_notBefore(bytes_compat(self.validity_start.strftime(generalized_time))) cert.set_notAfter(bytes_compat(self.validity_end.strftime(generalized_time))) # generating certificate for CA if not hasattr(self, 'ca'): issuer = cert.get_subject() issuer_key = key # generating certificate issued by a CA else: issuer = self.ca.x509.get_subject() issuer_key = self.ca.pkey cert.set_issuer(issuer) cert.set_pubkey(key) cert = self._add_extensions(cert) cert.sign(issuer_key, str(self.digest)) self.certificate = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8") self.private_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, key).decode("utf-8") def _fill_subject(self, subject): """ (internal use only) fills OpenSSL.crypto.X509Name object """ attr_map = { 'country_code': 'countryName', 'state': 'stateOrProvinceName', 'city': 'localityName', 'organization_name': 'organizationName', 'organizational_unit_name': 'organizationalUnitName', 'email': 'emailAddress', 'common_name': 'commonName' } # set x509 subject attributes only if not empty strings for model_attr, subject_attr in attr_map.items(): value = getattr(self, model_attr) if value: # coerce value to string, allow these fields to be redefined # as foreign keys by subclasses without losing compatibility if not isinstance(value, string_types): value = str(value) setattr(subject, subject_attr, value) return subject def _import(self): """ (internal use only) imports existing x509 certificates """ cert = self.x509 # when importing an end entity certificate if hasattr(self, 'ca'): self._verify_ca() self.key_length = str(cert.get_pubkey().bits()) # this line might fail if a certificate with # an unsupported signature algorithm is imported algorithm = cert.get_signature_algorithm().decode('utf8') self.digest = SIGNATURE_MAPPING[algorithm] not_before = cert.get_notBefore().decode('utf8') self.validity_start = datetime.strptime(not_before, generalized_time) self.validity_start = timezone.make_aware(self.validity_start) not_after = cert.get_notAfter().decode('utf8') self.validity_end = datetime.strptime(not_after, generalized_time) self.validity_end.replace(tzinfo=timezone.tzinfo()) self.validity_end = timezone.make_aware(self.validity_end) subject = cert.get_subject() self.country_code = subject.countryName or '' # allow importing from legacy systems which use invalid country codes if len(self.country_code) > 2: self.country_code = '' self.state = subject.stateOrProvinceName or '' self.city = subject.localityName or '' self.organization_name = subject.organizationName or '' self.organizational_unit_name = subject.organizationalUnitName or '' self.email = subject.emailAddress or '' self.common_name = subject.commonName or '' self.serial_number = cert.get_serial_number() if not self.name: self.name = self.common_name or str(self.serial_number) def _verify_ca(self): """ (internal use only) verifies the current x509 is signed by the associated CA """ store = crypto.X509Store() store.add_cert(self.ca.x509) store_ctx = crypto.X509StoreContext(store, self.x509) try: store_ctx.verify_certificate() except crypto.X509StoreContextError as e: raise ValidationError(_("CA doesn't match, got the " "following error from pyOpenSSL: \"%s\"") % e.args[0][2]) def _verify_extension_format(self): """ (internal use only) verifies the format of ``self.extension`` is correct """ msg = 'Extension format invalid' if not isinstance(self.extensions, list): raise ValidationError(msg) for ext in self.extensions: if not isinstance(ext, dict): raise ValidationError(msg) if not ('name' in ext and 'critical' in ext and 'value' in ext): raise ValidationError(msg) def _add_extensions(self, cert): """ (internal use only) adds x509 extensions to ``cert`` """ ext = [] # prepare extensions for CA if not hasattr(self, 'ca'): pathlen = app_settings.CA_BASIC_CONSTRAINTS_PATHLEN ext_value = 'CA:TRUE' if pathlen is not None: ext_value = '{0}, pathlen:{1}'.format(ext_value, pathlen) ext.append(crypto.X509Extension(b'basicConstraints', app_settings.CA_BASIC_CONSTRAINTS_CRITICAL, bytes_compat(ext_value))) ext.append(crypto.X509Extension(b'keyUsage', app_settings.CA_KEYUSAGE_CRITICAL, bytes_compat(app_settings.CA_KEYUSAGE_VALUE))) issuer_cert = cert # prepare extensions for end-entity certs else: ext.append(crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE')) ext.append(crypto.X509Extension(b'keyUsage', app_settings.CERT_KEYUSAGE_CRITICAL, bytes_compat(app_settings.CERT_KEYUSAGE_VALUE))) issuer_cert = self.ca.x509 ext.append(crypto.X509Extension(b'subjectKeyIdentifier', False, b'hash', subject=cert)) cert.add_extensions(ext) # authorityKeyIdentifier must be added after # the other extensions have been already added cert.add_extensions([ crypto.X509Extension(b'authorityKeyIdentifier', False, b'keyid:always,issuer:always', issuer=issuer_cert) ]) for ext in self.extensions: cert.add_extensions([ crypto.X509Extension(bytes_compat(ext['name']), bool(ext['critical']), bytes_compat(ext['value'])) ]) return cert class AbstractCa(BaseX509): """ Abstract Ca model """ class Meta: abstract = True verbose_name = _('CA') verbose_name_plural = _('CAs') def get_revoked_certs(self): """ Returns revoked certificates of this CA (does not include expired certificates) """ now = timezone.now() return self.cert_set.filter(revoked=True, validity_start__lte=now, validity_end__gte=now) @property def crl(self): """ Returns up to date CRL of this CA """ revoked_certs = self.get_revoked_certs() crl = crypto.CRL() now_str = timezone.now().strftime(generalized_time) for cert in revoked_certs: revoked = crypto.Revoked() revoked.set_serial(bytes_compat(cert.serial_number)) revoked.set_reason(b'unspecified') revoked.set_rev_date(bytes_compat(now_str)) crl.add_revoked(revoked) return crl.export(self.x509, self.pkey, days=1, digest=b'sha256') AbstractCa._meta.get_field('validity_end').default = default_ca_validity_end class AbstractCert(BaseX509): """ Abstract Cert model """ ca = models.ForeignKey('django_x509.Ca', on_delete=models.CASCADE, verbose_name=_('CA')) revoked = models.BooleanField(_('revoked'), default=False) revoked_at = models.DateTimeField(_('revoked at'), blank=True, null=True, default=None) def __str__(self): return self.name class Meta: abstract = True verbose_name = _('certificate') verbose_name_plural = _('certificates') unique_together = ('ca', 'serial_number') def revoke(self): """ * flag certificate as revoked * fill in revoked_at DateTimeField """ now = timezone.now() self.revoked = True self.revoked_at = now self.save() django-x509-0.4.1/django_x509/base/admin.py0000664000175000017500000001143113343027425021221 0ustar nemesisnemesis00000000000000from django import forms from django.contrib.admin import ModelAdmin from django.contrib.admin.templatetags.admin_static import static from django.urls import reverse from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ class X509Form(forms.ModelForm): OPERATION_CHOICES = ( ('-', '----- {0} -----'.format(_('Please select an option'))), ('new', _('Create new')), ('import', _('Import Existing')) ) operation_type = forms.ChoiceField(choices=OPERATION_CHOICES) class BaseAdmin(ModelAdmin): """ ModelAdmin for TimeStampedEditableModel """ list_display = ['name', 'key_length', 'digest', 'created', 'modified'] search_fields = ['name', 'serial_number', 'common_name'] actions_on_bottom = True save_on_top = True form = X509Form # custom attribute readonly_edit = ['key_length', 'digest', 'validity_start', 'validity_end', 'country_code', 'state', 'city', 'organization_name', 'organizational_unit_name', 'email', 'common_name', 'serial_number', 'certificate', 'private_key'] class Media: css = {'all': (static('django-x509/css/admin.css'),)} def __init__(self, *args, **kwargs): self.readonly_fields += ('created', 'modified') super(BaseAdmin, self).__init__(*args, **kwargs) def get_readonly_fields(self, request, obj=None): # edit if obj: return tuple(self.readonly_edit) + tuple(self.readonly_fields) # add else: return self.readonly_fields def get_fields(self, request, obj=None): fields = super(BaseAdmin, self).get_fields(request, obj) # edit if obj: fields = fields[:] # make copy fields.remove('extensions') return fields class AbstractCaAdmin(BaseAdmin): list_filter = ['key_length', 'digest', 'created'] fields = ['operation_type', 'name', 'notes', 'key_length', 'digest', 'validity_start', 'validity_end', 'country_code', 'state', 'city', 'organization_name', 'organizational_unit_name', 'email', 'common_name', 'extensions', 'serial_number', 'certificate', 'private_key', 'created', 'modified'] class Media: js = ('django-x509/js/x509-admin.js',) class AbstractCertAdmin(BaseAdmin): list_filter = ['ca', 'revoked', 'key_length', 'digest', 'created'] list_select_related = ['ca'] readonly_fields = ['revoked', 'revoked_at'] fields = ['operation_type', 'name', 'ca', 'notes', 'revoked', 'revoked_at', 'key_length', 'digest', 'validity_start', 'validity_end', 'country_code', 'state', 'city', 'organization_name', 'organizational_unit_name', 'email', 'common_name', 'extensions', 'serial_number', 'certificate', 'private_key', 'created', 'modified'] actions = ['revoke_action'] class Media: js = ('django-x509/js/x509-admin.js',) def ca_url(self, obj): url = reverse('admin:{0}_ca_change'.format(self.opts.app_label), args=[obj.ca.id]) return format_html("{text}", url=url, text=obj.ca.name) ca_url.short_description = 'CA' def revoke_action(self, request, queryset): rows = 0 for cert in queryset: cert.revoke() rows += 1 if rows == 1: bit = '1 certificate was' else: bit = '{0} certificates were'.format(rows) message = '{0} revoked.'.format(bit) self.message_user(request, _(message)) revoke_action.short_description = _('Revoke selected certificates') # For backward compatibility CaAdmin = AbstractCaAdmin CertAdmin = AbstractCertAdmin AbstractCertAdmin.list_display = BaseAdmin.list_display[:] AbstractCertAdmin.list_display.insert(1, 'ca_url') AbstractCertAdmin.list_display.insert(5, 'revoked') AbstractCertAdmin.readonly_edit = BaseAdmin.readonly_edit[:] AbstractCertAdmin.readonly_edit += ('ca',) django-x509-0.4.1/django_x509/base/__init__.py0000664000175000017500000000000013177564257021674 0ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/base/views.py0000664000175000017500000000127213234215531021264 0ustar nemesisnemesis00000000000000from django.http import HttpResponse from django.utils.translation import ugettext_lazy as _ from .. import settings as app_settings def crl(request, pk): """ returns CRL of a CA """ authenticated = request.user.is_authenticated authenticated = authenticated() if callable(authenticated) else authenticated if app_settings.CRL_PROTECTED and not authenticated: return HttpResponse(_('Forbidden'), status=403, content_type='text/plain') ca = crl.ca_model.objects.get(pk=pk) return HttpResponse(ca.crl, status=200, content_type='application/x-pem-file') django-x509-0.4.1/django_x509/views.py0000664000175000017500000000010613177564257020367 0ustar nemesisnemesis00000000000000from .base.views import crl from .models import Ca crl.ca_model = Ca django-x509-0.4.1/django_x509/migrations/0000775000175000017500000000000013343702155021021 5ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/migrations/0004_auto_20171207_1450.py0000664000175000017500000000150113212562242024433 0ustar nemesisnemesis00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.11.8 on 2017-12-07 13:50 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_x509', '0003_rename_organization_field'), ] operations = [ migrations.AlterField( model_name='ca', name='serial_number', field=models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=39, null=True, verbose_name='serial number'), ), migrations.AlterField( model_name='cert', name='serial_number', field=models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=39, null=True, verbose_name='serial number'), ), ] django-x509-0.4.1/django_x509/migrations/0002_certificate.py0000664000175000017500000000551613212562614024324 0ustar nemesisnemesis00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.9.5 on 2016-08-02 14:54 from __future__ import unicode_literals from django.db import migrations, models import django_x509.base.models class Migration(migrations.Migration): dependencies = [ ('django_x509', '0001_initial'), ] operations = [ migrations.RenameField( model_name='ca', old_name='public_key', new_name='certificate', ), migrations.RenameField( model_name='cert', old_name='public_key', new_name='certificate', ), migrations.AlterField( model_name='ca', name='digest', field=models.CharField(blank=True, choices=[('', ''), ('sha1', 'SHA1'), ('sha224', 'SHA224'), ('sha256', 'SHA256'), ('sha384', 'SHA384'), ('sha512', 'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm'), ), migrations.AlterField( model_name='ca', name='key_length', field=models.CharField(blank=True, choices=[('', ''), ('512', '512'), ('1024', '1024'), ('2048', '2048'), ('4096', '4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length'), ), migrations.AlterField( model_name='ca', name='private_key', field=models.TextField(blank=True, help_text='private key in X.509 PEM format'), ), migrations.AlterField( model_name='cert', name='digest', field=models.CharField(blank=True, choices=[('', ''), ('sha1', 'SHA1'), ('sha224', 'SHA224'), ('sha256', 'SHA256'), ('sha384', 'SHA384'), ('sha512', 'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm'), ), migrations.AlterField( model_name='cert', name='key_length', field=models.CharField(blank=True, choices=[('', ''), ('512', '512'), ('1024', '1024'), ('2048', '2048'), ('4096', '4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length'), ), migrations.AlterField( model_name='cert', name='private_key', field=models.TextField(blank=True, help_text='private key in X.509 PEM format'), ), migrations.AlterField( model_name='ca', name='certificate', field=models.TextField(blank=True, help_text='certificate in X.509 PEM format'), ), migrations.AlterField( model_name='cert', name='certificate', field=models.TextField(blank=True, help_text='certificate in X.509 PEM format'), ), ] django-x509-0.4.1/django_x509/migrations/__init__.py0000664000175000017500000000000013177564257023136 0ustar nemesisnemesis00000000000000django-x509-0.4.1/django_x509/migrations/0005_organizational_unit_name.py0000664000175000017500000000134313242534727027126 0ustar nemesisnemesis00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.11 on 2018-01-16 14:18 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_x509', '0004_auto_20171207_1450'), ] operations = [ migrations.AddField( model_name='ca', name='organizational_unit_name', field=models.CharField(blank=True, max_length=64, verbose_name='organizational unit name'), ), migrations.AddField( model_name='cert', name='organizational_unit_name', field=models.CharField(blank=True, max_length=64, verbose_name='organizational unit name'), ), ] django-x509-0.4.1/django_x509/migrations/0003_rename_organization_field.py0000664000175000017500000000111713177564257027251 0ustar nemesisnemesis00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.11.5 on 2017-11-03 11:31 from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('django_x509', '0002_certificate'), ] operations = [ migrations.RenameField( model_name='ca', old_name='organization', new_name='organization_name', ), migrations.RenameField( model_name='cert', old_name='organization', new_name='organization_name', ), ] django-x509-0.4.1/django_x509/migrations/0001_initial.py0000664000175000017500000001441413177564257023506 0ustar nemesisnemesis00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.9.7 on 2016-07-15 15:36 from __future__ import unicode_literals import django.db.models.deletion import django.utils.timezone import jsonfield.fields import model_utils.fields from django.db import migrations, models import django_x509.base.models class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='Ca', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=64)), ('notes', models.TextField(blank=True)), ('key_length', models.CharField(blank=True, choices=[(b'', b''), (b'512', b'512'), (b'1024', b'1024'), (b'2048', b'2048'), (b'4096', b'4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length')), ('digest', models.CharField(blank=True, choices=[(b'', b''), (b'sha1', b'SHA1'), (b'sha224', b'SHA224'), (b'sha256', b'SHA256'), (b'sha384', b'SHA384'), (b'sha512', b'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm')), ('validity_start', models.DateTimeField(blank=True, default=django_x509.base.models.default_validity_start, null=True)), ('validity_end', models.DateTimeField(blank=True, default=django_x509.base.models.default_ca_validity_end, null=True)), ('country_code', models.CharField(blank=True, max_length=2)), ('state', models.CharField(blank=True, max_length=64, verbose_name='state or province')), ('city', models.CharField(blank=True, max_length=64, verbose_name='city')), ('organization', models.CharField(blank=True, max_length=64, verbose_name='organization')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ('common_name', models.CharField(blank=True, max_length=63, verbose_name='common name')), ('extensions', jsonfield.fields.JSONField(blank=True, default=list, help_text='additional x509 certificate extensions', verbose_name='extensions')), ('serial_number', models.PositiveIntegerField(blank=True, help_text='leave blank to determine automatically', null=True, verbose_name='serial number')), ('public_key', models.TextField(blank=True, help_text=b'certificate in X.509 PEM format')), ('private_key', models.TextField(blank=True, help_text=b'private key in X.509 PEM format')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), ], options={ 'verbose_name': 'CA', 'verbose_name_plural': 'CAs', }, ), migrations.CreateModel( name='Cert', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=64)), ('notes', models.TextField(blank=True)), ('key_length', models.CharField(blank=True, choices=[(b'', b''), (b'512', b'512'), (b'1024', b'1024'), (b'2048', b'2048'), (b'4096', b'4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length')), ('digest', models.CharField(blank=True, choices=[(b'', b''), (b'sha1', b'SHA1'), (b'sha224', b'SHA224'), (b'sha256', b'SHA256'), (b'sha384', b'SHA384'), (b'sha512', b'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm')), ('validity_start', models.DateTimeField(blank=True, default=django_x509.base.models.default_validity_start, null=True)), ('validity_end', models.DateTimeField(blank=True, default=django_x509.base.models.default_cert_validity_end, null=True)), ('country_code', models.CharField(blank=True, max_length=2)), ('state', models.CharField(blank=True, max_length=64, verbose_name='state or province')), ('city', models.CharField(blank=True, max_length=64, verbose_name='city')), ('organization', models.CharField(blank=True, max_length=64, verbose_name='organization')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ('common_name', models.CharField(blank=True, max_length=63, verbose_name='common name')), ('extensions', jsonfield.fields.JSONField(blank=True, default=list, help_text='additional x509 certificate extensions', verbose_name='extensions')), ('serial_number', models.PositiveIntegerField(blank=True, help_text='leave blank to determine automatically', null=True, verbose_name='serial number')), ('public_key', models.TextField(blank=True, help_text=b'certificate in X.509 PEM format')), ('private_key', models.TextField(blank=True, help_text=b'private key in X.509 PEM format')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), ('revoked', models.BooleanField(default=False, verbose_name='revoked')), ('revoked_at', models.DateTimeField(blank=True, default=None, null=True, verbose_name='revoked at')), ('ca', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_x509.Ca', verbose_name='CA')), ], options={ 'verbose_name': 'certificate', 'verbose_name_plural': 'certificates', }, ), migrations.AlterUniqueTogether( name='cert', unique_together=set([('ca', 'serial_number')]), ), ] django-x509-0.4.1/requirements.txt0000664000175000017500000000015013343027425020076 0ustar nemesisnemesis00000000000000six django>=1.11,<2.2 django-model-utils jsonfield cryptography>=2.3.0,<2.4.0 pyopenssl>=17.5.0,<18.1.0