From 8df55f22aad4c5e1b6e630d83f5cbf941f357492 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 3 Mar 2020 23:35:25 +0100 Subject: [PATCH 1/4] crypto: implement simple certificate-key pair for easier management --- .../admin/templates/administration/base.html | 6 ++ .../certificatekeypair/list.html | 69 ++++++++++++++++ passbook/admin/urls.py | 22 +++++ passbook/admin/views/certificate_key_pair.py | 77 +++++++++++++++++ passbook/crypto/__init__.py | 0 passbook/crypto/admin.py | 5 ++ passbook/crypto/apps.py | 10 +++ passbook/crypto/builder.py | 82 +++++++++++++++++++ passbook/crypto/forms.py | 27 ++++++ passbook/crypto/migrations/0001_initial.py | 67 +++++++++++++++ passbook/crypto/migrations/__init__.py | 0 passbook/crypto/models.py | 50 +++++++++++ passbook/root/settings.py | 1 + 13 files changed, 416 insertions(+) create mode 100644 passbook/admin/templates/administration/certificatekeypair/list.html create mode 100644 passbook/admin/views/certificate_key_pair.py create mode 100644 passbook/crypto/__init__.py create mode 100644 passbook/crypto/admin.py create mode 100644 passbook/crypto/apps.py create mode 100644 passbook/crypto/builder.py create mode 100644 passbook/crypto/forms.py create mode 100644 passbook/crypto/migrations/0001_initial.py create mode 100644 passbook/crypto/migrations/__init__.py create mode 100644 passbook/crypto/models.py diff --git a/passbook/admin/templates/administration/base.html b/passbook/admin/templates/administration/base.html index 2be718d76..db483f1b9 100644 --- a/passbook/admin/templates/administration/base.html +++ b/passbook/admin/templates/administration/base.html @@ -64,6 +64,12 @@ {% trans 'Policies' %} +
  • + + {% trans 'Certificates' %} + +
  • diff --git a/passbook/admin/templates/administration/certificatekeypair/list.html b/passbook/admin/templates/administration/certificatekeypair/list.html new file mode 100644 index 000000000..bfc62f86e --- /dev/null +++ b/passbook/admin/templates/administration/certificatekeypair/list.html @@ -0,0 +1,69 @@ +{% extends "administration/base.html" %} + +{% load i18n %} +{% load utils %} + +{% block content %} +
    +
    +

    + + {% trans 'Certificate-Key Pairs' %} +

    +

    {% trans "Import certificates of external providers or create certificates to sign requests with." %}

    +
    +
    +
    +
    +
    + + {% include 'partials/pagination.html' %} +
    + + + + + + + + + + + + {% for kp in object_list %} + + + + + + + {% endfor %} + +
    {% trans 'Name' %}{% trans 'Private Key available' %}{% trans 'Fingerprint' %}{% trans 'Provider Type' %}
    +
    +
    {{ kp.name }}
    +
    +
    + + {% if kp.key_data is not None %} + {% trans 'Yes' %} + {% else %} + {% trans 'No' %} + {% endif %} + + + + {{ kp.fingerprint }} + + + {% trans 'Edit' %} + {% trans 'Delete' %} +
    +
    + {% include 'partials/pagination.html' %} +
    +
    +
    +{% endblock %} diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py index 51279ecad..7806471d7 100644 --- a/passbook/admin/urls.py +++ b/passbook/admin/urls.py @@ -4,6 +4,7 @@ from django.urls import path from passbook.admin.views import ( applications, audit, + certificate_key_pair, debug, factors, groups, @@ -148,6 +149,27 @@ urlpatterns = [ path( "group//delete/", groups.GroupDeleteView.as_view(), name="group-delete" ), + # Certificate-Key Pairs + path( + "crypto/certificates/", + certificate_key_pair.CertificateKeyPairListView.as_view(), + name="certificate_key_pair", + ), + path( + "crypto/certificates/create/", + certificate_key_pair.CertificateKeyPairCreateView.as_view(), + name="certificatekeypair-create", + ), + path( + "crypto/certificates//update/", + certificate_key_pair.CertificateKeyPairUpdateView.as_view(), + name="certificatekeypair-update", + ), + path( + "crypto/certificates//delete/", + certificate_key_pair.CertificateKeyPairDeleteView.as_view(), + name="certificatekeypair-delete", + ), # Audit Log path("audit/", audit.EventListView.as_view(), name="audit-log"), # Groups diff --git a/passbook/admin/views/certificate_key_pair.py b/passbook/admin/views/certificate_key_pair.py new file mode 100644 index 000000000..716434b84 --- /dev/null +++ b/passbook/admin/views/certificate_key_pair.py @@ -0,0 +1,77 @@ +"""passbook CertificateKeyPair administration""" +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin as DjangoPermissionRequiredMixin, +) +from django.contrib.messages.views import SuccessMessageMixin +from django.urls import reverse_lazy +from django.utils.translation import ugettext as _ +from django.views.generic import DeleteView, ListView, UpdateView +from guardian.mixins import PermissionListMixin, PermissionRequiredMixin + +from passbook.crypto.forms import CertificateKeyPairForm +from passbook.crypto.models import CertificateKeyPair +from passbook.lib.views import CreateAssignPermView + + +class CertificateKeyPairListView(LoginRequiredMixin, PermissionListMixin, ListView): + """Show list of all keypairs""" + + model = CertificateKeyPair + permission_required = "passbook_crypto.view_certificatekeypair" + ordering = "name" + paginate_by = 40 + template_name = "administration/certificatekeypair/list.html" + + +class CertificateKeyPairCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): + """Create new CertificateKeyPair""" + + model = CertificateKeyPair + form_class = CertificateKeyPairForm + permission_required = "passbook_crypto.add_certificatekeypair" + + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:certificate_key_pair") + success_message = _("Successfully created CertificateKeyPair") + + def get_context_data(self, **kwargs): + kwargs["type"] = "Certificate-Key Pair" + return super().get_context_data(**kwargs) + + +class CertificateKeyPairUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): + """Update certificatekeypair""" + + model = CertificateKeyPair + form_class = CertificateKeyPairForm + permission_required = "passbook_crypto.change_certificatekeypair" + + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:certificate_key_pair") + success_message = _("Successfully updated Certificate-Key Pair") + + +class CertificateKeyPairDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): + """Delete certificatekeypair""" + + model = CertificateKeyPair + permission_required = "passbook_crypto.delete_certificatekeypair" + + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:certificate_key_pair") + success_message = _("Successfully deleted Certificate-Key Pair") + + def delete(self, request, *args, **kwargs): + messages.success(self.request, self.success_message) + return super().delete(request, *args, **kwargs) diff --git a/passbook/crypto/__init__.py b/passbook/crypto/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/crypto/admin.py b/passbook/crypto/admin.py new file mode 100644 index 000000000..5179b0d07 --- /dev/null +++ b/passbook/crypto/admin.py @@ -0,0 +1,5 @@ +"""passbook crypto model admin""" + +from passbook.lib.admin import admin_autoregister + +admin_autoregister("passbook_crypto") diff --git a/passbook/crypto/apps.py b/passbook/crypto/apps.py new file mode 100644 index 000000000..fa41d0dc3 --- /dev/null +++ b/passbook/crypto/apps.py @@ -0,0 +1,10 @@ +"""passbook crypto app config""" +from django.apps import AppConfig + + +class PassbookCryptoConfig(AppConfig): + """passbook crypto app config""" + + name = "passbook.crypto" + label = "passbook_crypto" + verbose_name = "passbook Crypto" diff --git a/passbook/crypto/builder.py b/passbook/crypto/builder.py new file mode 100644 index 000000000..67545f5bb --- /dev/null +++ b/passbook/crypto/builder.py @@ -0,0 +1,82 @@ +"""Create self-signed certificates""" +import datetime +import uuid + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID + + +class CertificateBuilder: + """Build self-signed certificates""" + + __public_key = None + __private_key = None + __builder = None + __certificate = None + + def __init__(self): + self.__public_key = None + self.__private_key = None + self.__builder = None + self.__certificate = None + + def build(self): + """Build self-signed certificate""" + one_day = datetime.timedelta(1, 0, 0) + self.__private_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ) + self.__public_key = self.__private_key.public_key() + self.__builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, u"passbook Self-signed Certificate", + ), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"passbook"), + x509.NameAttribute( + NameOID.ORGANIZATIONAL_UNIT_NAME, u"Self-signed" + ), + ] + ) + ) + .issuer_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, u"passbook Self-signed Certificate", + ), + ] + ) + ) + .not_valid_before(datetime.datetime.today() - one_day) + .not_valid_after(datetime.datetime.today() + datetime.timedelta(days=365)) + .serial_number(int(uuid.uuid4())) + .public_key(self.__public_key) + ) + self.__certificate = self.__builder.sign( + private_key=self.__private_key, + algorithm=hashes.SHA256(), + backend=default_backend(), + ) + + @property + def private_key(self): + """Return private key in PEM format""" + return self.__private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ).decode("utf-8") + + @property + def certificate(self): + """Return certificate in PEM format""" + return self.__certificate.public_bytes( + encoding=serialization.Encoding.PEM, + ).decode("utf-8") diff --git a/passbook/crypto/forms.py b/passbook/crypto/forms.py new file mode 100644 index 000000000..cbfb5de23 --- /dev/null +++ b/passbook/crypto/forms.py @@ -0,0 +1,27 @@ +"""passbook Crypto forms""" +from django import forms +from django.utils.translation import gettext_lazy as _ + +from passbook.crypto.models import CertificateKeyPair + + +class CertificateKeyPairForm(forms.ModelForm): + """CertificateKeyPair Form""" + + class Meta: + + model = CertificateKeyPair + fields = [ + "name", + "certificate_data", + "key_data", + ] + widgets = { + "name": forms.TextInput(), + "certificate_data": forms.Textarea(attrs={"class": "monospaced"}), + "key_data": forms.Textarea(attrs={"class": "monospaced"}), + } + labels = { + "certificate_data": _("Certificate"), + "key_data": _("Private Key"), + } diff --git a/passbook/crypto/migrations/0001_initial.py b/passbook/crypto/migrations/0001_initial.py new file mode 100644 index 000000000..9e749f148 --- /dev/null +++ b/passbook/crypto/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# Generated by Django 3.0.3 on 2020-03-03 21:45 + +import uuid + +from django.db import migrations, models + + +def create_self_signed(apps, schema_editor): + CertificateKeyPair = apps.get_model("passbook_crypto", "CertificateKeyPair") + db_alias = schema_editor.connection.alias + from passbook.crypto.builder import CertificateBuilder + + builder = CertificateBuilder() + builder.build() + CertificateKeyPair.objects.using(db_alias).create( + name="passbook Self-signed Certificate", + certificate_data=builder.certificate, + key_data=builder.private_key, + ) + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="CertificateKeyPair", + fields=[ + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("name", models.TextField()), + ("certificate_data", models.TextField()), + ("key_data", models.TextField(blank=True, default="")), + ], + options={ + "verbose_name": "Certificate-Key Pair", + "verbose_name_plural": "Certificate-Key Pairs", + }, + ), + migrations.RunPython(create_self_signed), + migrations.AlterField( + model_name="certificatekeypair", + name="certificate_data", + field=models.TextField(help_text="PEM-encoded Certificate data"), + ), + migrations.AlterField( + model_name="certificatekeypair", + name="key_data", + field=models.TextField( + blank=True, + default="", + help_text="Optional Private Key. If this is set, you can use this keypair for encryption.", + ), + ), + ] diff --git a/passbook/crypto/migrations/__init__.py b/passbook/crypto/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/crypto/models.py b/passbook/crypto/models.py new file mode 100644 index 000000000..80bd7f29b --- /dev/null +++ b/passbook/crypto/models.py @@ -0,0 +1,50 @@ +"""passbook crypto models""" +from binascii import hexlify +from typing import Optional + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.x509 import Certificate, load_pem_x509_certificate +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from passbook.lib.models import CreatedUpdatedModel, UUIDModel + + +class CertificateKeyPair(UUIDModel, CreatedUpdatedModel): + """CertificateKeyPair that can be used for signing or encrypting if `key_data` + is set, otherwise it can be used to verify remote data.""" + + name = models.TextField() + certificate_data = models.TextField(help_text=_("PEM-encoded Certificate data")) + key_data = models.TextField( + help_text=_( + "Optional Private Key. If this is set, you can use this keypair for encryption." + ), + blank=True, + default="", + ) + + _cert: Optional[Certificate] = None + + @property + def certificate(self) -> Certificate: + """Get python cryptography Certificate instance""" + if not self._cert: + self._cert = load_pem_x509_certificate( + self.certificate_data.encode("utf-8"), default_backend() + ) + return self._cert + + @property + def fingerprint(self) -> str: + """Get SHA256 Fingerprint of certificate_data""" + return hexlify(self.certificate.fingerprint(hashes.SHA256())).decode("utf-8") + + def __str__(self) -> str: + return f"Certificate-Key Pair {self.name} {self.fingerprint}" + + class Meta: + + verbose_name = _("Certificate-Key Pair") + verbose_name_plural = _("Certificate-Key Pairs") diff --git a/passbook/root/settings.py b/passbook/root/settings.py index 503d8da13..b60197ab7 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -85,6 +85,7 @@ INSTALLED_APPS = [ "passbook.api.apps.PassbookAPIConfig", "passbook.lib.apps.PassbookLibConfig", "passbook.audit.apps.PassbookAuditConfig", + "passbook.crypto.apps.PassbookCryptoConfig", "passbook.recovery.apps.PassbookRecoveryConfig", "passbook.sources.saml.apps.PassbookSourceSAMLConfig", "passbook.sources.ldap.apps.PassbookSourceLDAPConfig", From dc8b89a6b92e4997465e0f701e3611bcd2e3e669 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 3 Mar 2020 23:35:38 +0100 Subject: [PATCH 2/4] sources/saml: switch to new crypto --- passbook/sources/saml/api.py | 2 +- passbook/sources/saml/forms.py | 9 +------ .../migrations/0006_auto_20200303_2201.py | 27 +++++++++++++++++++ passbook/sources/saml/models.py | 12 ++++++++- passbook/sources/saml/processors/base.py | 2 +- passbook/sources/saml/views.py | 6 ++--- 6 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 passbook/sources/saml/migrations/0006_auto_20200303_2201.py diff --git a/passbook/sources/saml/api.py b/passbook/sources/saml/api.py index d89014d4a..53d741fef 100644 --- a/passbook/sources/saml/api.py +++ b/passbook/sources/saml/api.py @@ -17,7 +17,7 @@ class SAMLSourceSerializer(ModelSerializer): "idp_url", "idp_logout_url", "auto_logout", - "signing_cert", + "signing_kp", ] diff --git a/passbook/sources/saml/forms.py b/passbook/sources/saml/forms.py index ff6ed84ee..cfc3bf2c5 100644 --- a/passbook/sources/saml/forms.py +++ b/passbook/sources/saml/forms.py @@ -5,19 +5,12 @@ from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext as _ from passbook.admin.forms.source import SOURCE_FORM_FIELDS -from passbook.providers.saml.utils.cert import CertificateBuilder from passbook.sources.saml.models import SAMLSource class SAMLSourceForm(forms.ModelForm): """SAML Provider form""" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - builder = CertificateBuilder() - builder.build() - self.fields["signing_cert"].initial = builder.certificate - class Meta: model = SAMLSource @@ -26,7 +19,7 @@ class SAMLSourceForm(forms.ModelForm): "idp_url", "idp_logout_url", "auto_logout", - "signing_cert", + "signing_kp", ] widgets = { "name": forms.TextInput(), diff --git a/passbook/sources/saml/migrations/0006_auto_20200303_2201.py b/passbook/sources/saml/migrations/0006_auto_20200303_2201.py new file mode 100644 index 000000000..7a152b2c3 --- /dev/null +++ b/passbook/sources/saml/migrations/0006_auto_20200303_2201.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.3 on 2020-03-03 22:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_crypto", "0001_initial"), + ("passbook_sources_saml", "0005_auto_20200220_1621"), + ] + + operations = [ + migrations.RemoveField(model_name="samlsource", name="signing_cert",), + migrations.AddField( + model_name="samlsource", + name="signing_kp", + field=models.ForeignKey( + default=None, + help_text="Certificate Key Pair of the IdP which Assertions are validated against.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="passbook_crypto.CertificateKeyPair", + ), + ), + ] diff --git a/passbook/sources/saml/models.py b/passbook/sources/saml/models.py index 4f8127bec..f98133ffc 100644 --- a/passbook/sources/saml/models.py +++ b/passbook/sources/saml/models.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _ from passbook.core.models import Source from passbook.core.types import UILoginButton +from passbook.crypto.models import CertificateKeyPair class SAMLSource(Source): @@ -22,7 +23,16 @@ class SAMLSource(Source): default=None, blank=True, null=True, verbose_name=_("IDP Logout URL") ) auto_logout = models.BooleanField(default=False) - signing_cert = models.TextField() + + signing_kp = models.ForeignKey( + CertificateKeyPair, + default=None, + null=True, + help_text=_( + "Certificate Key Pair of the IdP which Assertions are validated against." + ), + on_delete=models.SET_NULL, + ) form = "passbook.sources.saml.forms.SAMLSourceForm" diff --git a/passbook/sources/saml/processors/base.py b/passbook/sources/saml/processors/base.py index 775791adf..5568d9286 100644 --- a/passbook/sources/saml/processors/base.py +++ b/passbook/sources/saml/processors/base.py @@ -46,7 +46,7 @@ class Processor: def _verify_signed(self): """Verify SAML Response's Signature""" verifier = XMLVerifier() - verifier.verify(self._root_xml, x509_cert=self._source.signing_cert) + verifier.verify(self._root_xml, x509_cert=self._source.signing_kp.certificate) def _get_email(self) -> Optional[str]: """ diff --git a/passbook/sources/saml/views.py b/passbook/sources/saml/views.py index 9f6516afa..6dd38db93 100644 --- a/passbook/sources/saml/views.py +++ b/passbook/sources/saml/views.py @@ -101,9 +101,9 @@ class MetadataView(View): """Replies with the XML Metadata SPSSODescriptor.""" source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) issuer = get_issuer(request, source) - cert_stripped = strip_pem_header(source.signing_cert.replace("\r", "")).replace( - "\n", "" - ) + cert_stripped = strip_pem_header( + source.signing_kp.certificate_data.replace("\r", "") + ).replace("\n", "") return render_xml( request, "saml/sp/xml/sp_sso_descriptor.xml", From 80a50f9bdb775c33e41d93258e00b397ea05598d Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 3 Mar 2020 23:35:50 +0100 Subject: [PATCH 3/4] providers/saml: switch to new crypto --- passbook/providers/saml/api.py | 4 +- passbook/providers/saml/forms.py | 12 +-- .../migrations/0007_auto_20200303_2157.py | 29 +++++++ passbook/providers/saml/models.py | 11 ++- passbook/providers/saml/utils/cert.py | 84 ------------------- passbook/providers/saml/utils/xml_signing.py | 7 +- passbook/providers/saml/views.py | 6 +- 7 files changed, 47 insertions(+), 106 deletions(-) create mode 100644 passbook/providers/saml/migrations/0007_auto_20200303_2157.py delete mode 100644 passbook/providers/saml/utils/cert.py diff --git a/passbook/providers/saml/api.py b/passbook/providers/saml/api.py index 1348c85ca..1f8d4d6ec 100644 --- a/passbook/providers/saml/api.py +++ b/passbook/providers/saml/api.py @@ -24,9 +24,7 @@ class SAMLProviderSerializer(ModelSerializer): "property_mappings", "digest_algorithm", "signature_algorithm", - "signing", - "signing_cert", - "signing_key", + "singing_kp", ] diff --git a/passbook/providers/saml/forms.py b/passbook/providers/saml/forms.py index 08dc95f89..e2178f1f6 100644 --- a/passbook/providers/saml/forms.py +++ b/passbook/providers/saml/forms.py @@ -9,7 +9,6 @@ from passbook.providers.saml.models import ( SAMLProvider, get_provider_choices, ) -from passbook.providers.saml.utils.cert import CertificateBuilder class SAMLProviderForm(forms.ModelForm): @@ -19,13 +18,6 @@ class SAMLProviderForm(forms.ModelForm): choices=get_provider_choices(), label="Processor" ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - builder = CertificateBuilder() - builder.build() - self.fields["signing_cert"].initial = builder.certificate - self.fields["signing_key"].initial = builder.private_key - class Meta: model = SAMLProvider @@ -41,9 +33,7 @@ class SAMLProviderForm(forms.ModelForm): "property_mappings", "digest_algorithm", "signature_algorithm", - "signing", - "signing_cert", - "signing_key", + "singing_kp", ] widgets = { "name": forms.TextInput(), diff --git a/passbook/providers/saml/migrations/0007_auto_20200303_2157.py b/passbook/providers/saml/migrations/0007_auto_20200303_2157.py new file mode 100644 index 000000000..d9a3400d1 --- /dev/null +++ b/passbook/providers/saml/migrations/0007_auto_20200303_2157.py @@ -0,0 +1,29 @@ +# Generated by Django 3.0.3 on 2020-03-03 21:57 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_crypto", "0001_initial"), + ("passbook_providers_saml", "0006_auto_20200217_2031"), + ] + + operations = [ + migrations.RemoveField(model_name="samlprovider", name="signing",), + migrations.RemoveField(model_name="samlprovider", name="signing_cert",), + migrations.RemoveField(model_name="samlprovider", name="signing_key",), + migrations.AddField( + model_name="samlprovider", + name="singing_kp", + field=models.ForeignKey( + default=None, + help_text="Singing is enabled upon selection of a Key Pair.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="passbook_crypto.CertificateKeyPair", + ), + ), + ] diff --git a/passbook/providers/saml/models.py b/passbook/providers/saml/models.py index 42e97328e..d215f1a9b 100644 --- a/passbook/providers/saml/models.py +++ b/passbook/providers/saml/models.py @@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from structlog import get_logger from passbook.core.models import PropertyMapping, Provider +from passbook.crypto.models import CertificateKeyPair from passbook.lib.utils.reflection import class_to_path, path_to_class from passbook.lib.utils.template import render_to_string from passbook.providers.saml.processors.base import Processor @@ -74,9 +75,13 @@ class SAMLProvider(Provider): default="rsa-sha256", ) - signing = models.BooleanField(default=True) - signing_cert = models.TextField(verbose_name=_("Singing Certificate")) - signing_key = models.TextField() + singing_kp = models.ForeignKey( + CertificateKeyPair, + default=None, + null=True, + help_text=_("Singing is enabled upon selection of a Key Pair."), + on_delete=models.SET_NULL, + ) form = "passbook.providers.saml.forms.SAMLProviderForm" _processor = None diff --git a/passbook/providers/saml/utils/cert.py b/passbook/providers/saml/utils/cert.py deleted file mode 100644 index 9a0b6c56f..000000000 --- a/passbook/providers/saml/utils/cert.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Create self-signed certificates""" -import datetime -import uuid - -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.x509.oid import NameOID - - -class CertificateBuilder: - """Build self-signed certificates""" - - __public_key = None - __private_key = None - __builder = None - __certificate = None - - def __init__(self): - self.__public_key = None - self.__private_key = None - self.__builder = None - self.__certificate = None - - def build(self): - """Build self-signed certificate""" - one_day = datetime.timedelta(1, 0, 0) - self.__private_key = rsa.generate_private_key( - public_exponent=65537, key_size=2048, backend=default_backend() - ) - self.__public_key = self.__private_key.public_key() - self.__builder = ( - x509.CertificateBuilder() - .subject_name( - x509.Name( - [ - x509.NameAttribute( - NameOID.COMMON_NAME, - u"passbook Self-signed SAML Certificate", - ), - x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"passbook"), - x509.NameAttribute( - NameOID.ORGANIZATIONAL_UNIT_NAME, u"Self-signed" - ), - ] - ) - ) - .issuer_name( - x509.Name( - [ - x509.NameAttribute( - NameOID.COMMON_NAME, - u"passbook Self-signed SAML Certificate", - ), - ] - ) - ) - .not_valid_before(datetime.datetime.today() - one_day) - .not_valid_after(datetime.datetime.today() + datetime.timedelta(days=365)) - .serial_number(int(uuid.uuid4())) - .public_key(self.__public_key) - ) - self.__certificate = self.__builder.sign( - private_key=self.__private_key, - algorithm=hashes.SHA256(), - backend=default_backend(), - ) - - @property - def private_key(self): - """Return private key in PEM format""" - return self.__private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ).decode("utf-8") - - @property - def certificate(self): - """Return certificate in PEM format""" - return self.__certificate.public_bytes( - encoding=serialization.Encoding.PEM, - ).decode("utf-8") diff --git a/passbook/providers/saml/utils/xml_signing.py b/passbook/providers/saml/utils/xml_signing.py index 496e48f8a..3afa2bdff 100644 --- a/passbook/providers/saml/utils/xml_signing.py +++ b/passbook/providers/saml/utils/xml_signing.py @@ -31,9 +31,12 @@ def sign_with_signxml(data: str, provider: "SAMLProvider", reference_uri=None) - digest_algorithm=provider.digest_algorithm, ) signed = signer.sign( - root, key=key, cert=[provider.signing_cert], reference_uri=reference_uri + root, + key=key, + cert=[provider.singing_kp.certificate_data], + reference_uri=reference_uri, ) - XMLVerifier().verify(signed, x509_cert=provider.signing_cert) + XMLVerifier().verify(signed, x509_cert=provider.singing_kp.certificate_data) return etree.tostring(signed).decode("utf-8") # nosec diff --git a/passbook/providers/saml/views.py b/passbook/providers/saml/views.py index 793cfeab9..3a3dc0665 100644 --- a/passbook/providers/saml/views.py +++ b/passbook/providers/saml/views.py @@ -274,9 +274,9 @@ class DescriptorDownloadView(AccessRequiredView): kwargs={"application": provider.application.slug}, ) ) - pubkey = strip_pem_header(provider.signing_cert.replace("\r", "")).replace( - "\n", "" - ) + pubkey = strip_pem_header( + provider.singing_kp.certificate_data.replace("\r", "") + ).replace("\n", "") subject_format = provider.processor.subject_format ctx = { "entity_id": entity_id, From f2154d9875c57b468a8de26eedc48793d98db708 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 4 Mar 2020 19:43:18 +0100 Subject: [PATCH 4/4] crypto: add property for private_key --- passbook/crypto/models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/passbook/crypto/models.py b/passbook/crypto/models.py index 80bd7f29b..e3ec5c607 100644 --- a/passbook/crypto/models.py +++ b/passbook/crypto/models.py @@ -4,6 +4,8 @@ from typing import Optional from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.x509 import Certificate, load_pem_x509_certificate from django.db import models from django.utils.translation import gettext_lazy as _ @@ -26,6 +28,7 @@ class CertificateKeyPair(UUIDModel, CreatedUpdatedModel): ) _cert: Optional[Certificate] = None + _key: Optional[RSAPrivateKey] = None @property def certificate(self) -> Certificate: @@ -36,6 +39,17 @@ class CertificateKeyPair(UUIDModel, CreatedUpdatedModel): ) return self._cert + @property + def private_key(self) -> Optional[RSAPrivateKey]: + """Get python cryptography PrivateKey instance""" + if not self._key: + self._key = load_pem_private_key( + str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])), + password=None, + backend=default_backend(), + ) + return self._key + @property def fingerprint(self) -> str: """Get SHA256 Fingerprint of certificate_data"""