diff --git a/passbook/providers/saml/api.py b/passbook/providers/saml/api.py index 6a5baf748..0f1b950f3 100644 --- a/passbook/providers/saml/api.py +++ b/passbook/providers/saml/api.py @@ -25,6 +25,7 @@ class SAMLProviderSerializer(ModelSerializer): "digest_algorithm", "signature_algorithm", "signing_kp", + "require_signing", ] diff --git a/passbook/providers/saml/forms.py b/passbook/providers/saml/forms.py index 1fa0e7adf..76a56d66a 100644 --- a/passbook/providers/saml/forms.py +++ b/passbook/providers/saml/forms.py @@ -32,6 +32,7 @@ class SAMLProviderForm(forms.ModelForm): "session_valid_not_on_or_after", "property_mappings", "digest_algorithm", + "require_signing", "signature_algorithm", "signing_kp", ] diff --git a/passbook/providers/saml/migrations/0009_auto_20200506_1551.py b/passbook/providers/saml/migrations/0009_auto_20200506_1551.py new file mode 100644 index 000000000..5ff87d9e5 --- /dev/null +++ b/passbook/providers/saml/migrations/0009_auto_20200506_1551.py @@ -0,0 +1,40 @@ +# Generated by Django 3.0.3 on 2020-05-06 15:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_crypto", "0001_initial"), + ("passbook_providers_saml", "0008_auto_20200305_1606"), + ] + + operations = [ + migrations.AddField( + model_name="samlprovider", + name="require_signing", + field=models.BooleanField( + default=False, + help_text="Require Requests to be signed by an X509 Certificate. Must match the Certificate selected in `Singing Keypair`.", + ), + ), + migrations.AlterField( + model_name="samlprovider", + name="issuer", + field=models.TextField(help_text="Also known as EntityID"), + ), + migrations.AlterField( + model_name="samlprovider", + name="signing_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", + verbose_name="Signing Keypair", + ), + ), + ] diff --git a/passbook/providers/saml/models.py b/passbook/providers/saml/models.py index 1007b71d2..f48eaba49 100644 --- a/passbook/providers/saml/models.py +++ b/passbook/providers/saml/models.py @@ -25,7 +25,7 @@ class SAMLProvider(Provider): acs_url = models.URLField(verbose_name=_("ACS URL")) audience = models.TextField(default="") - issuer = models.TextField() + issuer = models.TextField(help_text=_("Also known as EntityID")) assertion_valid_not_before = models.TextField( default="minutes=-5", @@ -81,6 +81,15 @@ class SAMLProvider(Provider): null=True, help_text=_("Singing is enabled upon selection of a Key Pair."), on_delete=models.SET_NULL, + verbose_name=_("Signing Keypair"), + ) + + require_signing = models.BooleanField( + default=False, + help_text=_( + "Require Requests to be signed by an X509 Certificate. " + "Must match the Certificate selected in `Singing Keypair`." + ), ) form = "passbook.providers.saml.forms.SAMLProviderForm" diff --git a/passbook/providers/saml/processors/base.py b/passbook/providers/saml/processors/base.py index 78abd20cd..c187ebbf8 100644 --- a/passbook/providers/saml/processors/base.py +++ b/passbook/providers/saml/processors/base.py @@ -1,8 +1,10 @@ """Basic SAML Processor""" from typing import TYPE_CHECKING, Dict, List, Union +from cryptography.exceptions import InvalidSignature from defusedxml import ElementTree from django.http import HttpRequest +from signxml import XMLVerifier from structlog import get_logger from passbook.core.exceptions import PropertyMappingExpressionException @@ -146,6 +148,15 @@ class Processor: """Parses various parameters from _request_xml into _request_params.""" decoded_xml = decode_base64_and_inflate(self._saml_request) + if self._remote.require_signing and self._remote.signing_kp: + self._logger.debug("Verifying Request signature") + try: + XMLVerifier().verify( + decoded_xml, x509_cert=self._remote.signing_kp.certificate_data + ) + except InvalidSignature as exc: + raise CannotHandleAssertion("Failed to verify signature") from exc + root = ElementTree.fromstring(decoded_xml) params = {}