django-x509-0.3.4/0000775000175000017500000000000013216512256014620 5ustar nemesisnemesis00000000000000django-x509-0.3.4/requirements.txt0000664000175000017500000000015113214250556020101 0ustar nemesisnemesis00000000000000six django>=1.10,<1.12 django-model-utils jsonfield cryptography>=2.1.4,<2.2.0 pyopenssl>=17.5.0,<17.6.0 django-x509-0.3.4/README.rst0000664000175000017500000002515113214174601016307 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. ------------ .. 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. 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 ------------ 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.3.4/django_x509.egg-info/0000775000175000017500000000000013216512256020341 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509.egg-info/SOURCES.txt0000664000175000017500000000200513216512256022222 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/__init__.py django_x509/static/django-x509/css/admin.css django_x509/static/django-x509/js/switcher.js django_x509/templates/admin/django_x509/change_form.html django_x509/tests/__init__.py django_x509/tests/test_ca.py django_x509/tests/test_cert.pydjango-x509-0.3.4/django_x509.egg-info/top_level.txt0000664000175000017500000000001413216512256023066 0ustar nemesisnemesis00000000000000django_x509 django-x509-0.3.4/django_x509.egg-info/not-zip-safe0000664000175000017500000000000112737727603022602 0ustar nemesisnemesis00000000000000 django-x509-0.3.4/django_x509.egg-info/requires.txt0000664000175000017500000000015113216512256022736 0ustar nemesisnemesis00000000000000six django<1.12,>=1.10 django-model-utils jsonfield cryptography<2.2.0,>=2.1.4 pyopenssl<17.6.0,>=17.5.0 django-x509-0.3.4/django_x509.egg-info/dependency_links.txt0000664000175000017500000000000113216512256024407 0ustar nemesisnemesis00000000000000 django-x509-0.3.4/django_x509.egg-info/PKG-INFO0000664000175000017500000003553713216512256021453 0ustar nemesisnemesis00000000000000Metadata-Version: 1.1 Name: django-x509 Version: 0.3.4 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-Content-Type: UNKNOWN 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. ------------ .. 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. 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 ------------ 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.3.4/setup.cfg0000664000175000017500000000024113216512256016436 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.3.4/LICENSE0000664000175000017500000000272712737721324015642 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.3.4/django_x509/0000775000175000017500000000000013216512256016647 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/static/0000775000175000017500000000000013216512256020136 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/static/django-x509/0000775000175000017500000000000013216512256022103 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/static/django-x509/js/0000775000175000017500000000000013216512256022517 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/static/django-x509/js/switcher.js0000664000175000017500000000230213216503140024672 0ustar nemesisnemesis00000000000000django.jQuery(function ($) { 'use strict'; 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.3.4/django_x509/static/django-x509/css/0000775000175000017500000000000013216512256022673 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/static/django-x509/css/admin.css0000664000175000017500000000107613176651542024510 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.3.4/django_x509/templates/0000775000175000017500000000000013216512256020645 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/templates/admin/0000775000175000017500000000000013216512256021735 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/templates/admin/django_x509/0000775000175000017500000000000013216512256023764 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/templates/admin/django_x509/change_form.html0000664000175000017500000000104413035453372027123 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.3.4/django_x509/settings.py0000644000175000017500000000201113053315245021047 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.3.4/django_x509/__init__.py0000664000175000017500000000070613216511670020762 0ustar nemesisnemesis00000000000000VERSION = (0, 3, 4, '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.3.4/django_x509/migrations/0000775000175000017500000000000013216512256021023 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/migrations/0002_certificate.py0000664000175000017500000000551613214250556024327 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.3.4/django_x509/migrations/__init__.py0000664000175000017500000000000012737501054023123 0ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/migrations/0004_auto_20171207_1450.py0000664000175000017500000000150113214174601024435 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.3.4/django_x509/migrations/0001_initial.py0000664000175000017500000001441413106354604023471 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.3.4/django_x509/migrations/0003_rename_organization_field.py0000664000175000017500000000111713177123313027233 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.3.4/django_x509/views.py0000644000175000017500000000010613053315245020347 0ustar nemesisnemesis00000000000000from .base.views import crl from .models import Ca crl.ca_model = Ca django-x509-0.3.4/django_x509/apps.py0000664000175000017500000000030213053315245020155 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.3.4/django_x509/models.py0000664000175000017500000000045213053315245020503 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.3.4/django_x509/admin.py0000664000175000017500000000043413216505323020307 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.3.4/django_x509/base/0000775000175000017500000000000013216512256017561 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/base/__init__.py0000664000175000017500000000000013034733670021663 0ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/base/views.py0000644000175000017500000000111013053315245021255 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 """ if app_settings.CRL_PROTECTED and not request.user.is_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.3.4/django_x509/base/models.py0000664000175000017500000004244513214250556021427 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) intentionally returns naive datetime (not timezone aware) """ 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) 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) self.private_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) 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', '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.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.3.4/django_x509/base/admin.py0000664000175000017500000001117613216511474021232 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', '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 and 'extensions' in fields: 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', 'email', 'common_name', 'extensions', 'serial_number', 'certificate', 'private_key', 'created', 'modified'] class Media: js = ('django-x509/js/switcher.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', 'email', 'common_name', 'extensions', 'serial_number', 'certificate', 'private_key', 'created', 'modified'] actions = ['revoke_action'] class Media: js = ('django-x509/js/switcher.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.3.4/django_x509/utils.py0000644000175000017500000000041413053315245020354 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.3.4/django_x509/urls.py0000644000175000017500000000021113053315245020174 0ustar nemesisnemesis00000000000000from django.conf.urls import url from . import views urlpatterns = [ url(r'^x509/ca/(?P[^/]+).crl$', views.crl, name='crl'), ] django-x509-0.3.4/django_x509/tests/0000775000175000017500000000000013216512256020011 5ustar nemesisnemesis00000000000000django-x509-0.3.4/django_x509/tests/__init__.py0000664000175000017500000000270513177127104022126 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.3.4/django_x509/tests/test_ca.py0000664000175000017500000005477513214174601022023 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.3.4/django_x509/tests/test_cert.py0000664000175000017500000004111713214174601022357 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])) django-x509-0.3.4/setup.py0000664000175000017500000000421313105143040016316 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.3.4/PKG-INFO0000664000175000017500000003553713216512256015732 0ustar nemesisnemesis00000000000000Metadata-Version: 1.1 Name: django-x509 Version: 0.3.4 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-Content-Type: UNKNOWN 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. ------------ .. 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. 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 ------------ 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.3.4/MANIFEST.in0000664000175000017500000000040312737720347016364 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.3.4/CHANGES.rst0000664000175000017500000000547413216511665016437 0ustar nemesisnemesis00000000000000Changelog ========= 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