From a5629c51550f3b5c6a7e50177dbdd9427bea4d46 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 17 Feb 2020 16:28:18 +0100 Subject: [PATCH] providers/saml: add changeable signature and digest algorithm --- passbook/providers/saml/forms.py | 2 + .../migrations/0004_auto_20200217_1526.py | 41 +++++++++++++++++++ passbook/providers/saml/models.py | 16 ++++++++ passbook/providers/saml/utils/xml_render.py | 7 +--- passbook/providers/saml/utils/xml_signing.py | 23 ++++++++--- 5 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 passbook/providers/saml/migrations/0004_auto_20200217_1526.py diff --git a/passbook/providers/saml/forms.py b/passbook/providers/saml/forms.py index 778a0e273..ed7f79d6e 100644 --- a/passbook/providers/saml/forms.py +++ b/passbook/providers/saml/forms.py @@ -40,6 +40,8 @@ class SAMLProviderForm(forms.ModelForm): "assertion_valid_not_on_or_after", "session_valid_not_on_or_after", "property_mappings", + "digest_algorithm", + "signature_algorithm", "signing", "signing_cert", "signing_key", diff --git a/passbook/providers/saml/migrations/0004_auto_20200217_1526.py b/passbook/providers/saml/migrations/0004_auto_20200217_1526.py new file mode 100644 index 000000000..b6f29061a --- /dev/null +++ b/passbook/providers/saml/migrations/0004_auto_20200217_1526.py @@ -0,0 +1,41 @@ +# Generated by Django 3.0.3 on 2020-02-17 15:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_providers_saml", "0003_auto_20200216_1109"), + ] + + operations = [ + migrations.AddField( + model_name="samlprovider", + name="digest_algorithm", + field=models.CharField( + choices=[("sha1", "SHA1"), ("sha256", "SHA256")], + default="sha256", + max_length=50, + ), + ), + migrations.AddField( + model_name="samlprovider", + name="signature_algorithm", + field=models.CharField( + choices=[ + ("rsa-sha1", "RSA-SHA1"), + ("rsa-sha256", "RSA-SHA256"), + ("ecdsa-sha256", "ECDSA-SHA256"), + ("dsa-sha1", "DSA-SHA1"), + ], + default="rsa-sha256", + max_length=50, + ), + ), + migrations.AlterField( + model_name="samlprovider", + name="processor_path", + field=models.CharField(choices=[], max_length=255), + ), + ] diff --git a/passbook/providers/saml/models.py b/passbook/providers/saml/models.py index 7ccc4d2fa..0b0d66870 100644 --- a/passbook/providers/saml/models.py +++ b/passbook/providers/saml/models.py @@ -55,6 +55,22 @@ class SAMLProvider(Provider): ), ) + digest_algorithm = models.CharField( + max_length=50, + choices=(("sha1", _("SHA1")), ("sha256", _("SHA256")),), + default="sha256", + ) + signature_algorithm = models.CharField( + max_length=50, + choices=( + ("rsa-sha1", _("RSA-SHA1")), + ("rsa-sha256", _("RSA-SHA256")), + ("ecdsa-sha256", _("ECDSA-SHA256")), + ("dsa-sha1", _("DSA-SHA1")), + ), + default="rsa-sha256", + ) + signing = models.BooleanField(default=True) signing_cert = models.TextField(verbose_name=_("Singing Certificate")) signing_key = models.TextField() diff --git a/passbook/providers/saml/utils/xml_render.py b/passbook/providers/saml/utils/xml_render.py index f89a06354..9065c951a 100644 --- a/passbook/providers/saml/utils/xml_render.py +++ b/passbook/providers/saml/utils/xml_render.py @@ -88,10 +88,5 @@ def get_response_xml(parameters, saml_provider: SAMLProvider, assertion_id=""): signature_xml = get_signature_xml() params["RESPONSE_SIGNATURE"] = signature_xml - signed = sign_with_signxml( - saml_provider.signing_key, - raw_response, - saml_provider.signing_cert, - reference_uri=assertion_id, - ) + signed = sign_with_signxml(raw_response, saml_provider, reference_uri=assertion_id,) return signed diff --git a/passbook/providers/saml/utils/xml_signing.py b/passbook/providers/saml/utils/xml_signing.py index cd5669f2b..496e48f8a 100644 --- a/passbook/providers/saml/utils/xml_signing.py +++ b/passbook/providers/saml/utils/xml_signing.py @@ -1,4 +1,6 @@ """Signing code goes here.""" +from typing import TYPE_CHECKING + from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from lxml import etree # nosec @@ -7,25 +9,34 @@ from structlog import get_logger from passbook.lib.utils.template import render_to_string +if TYPE_CHECKING: + from passbook.providers.saml.models import SAMLProvider + LOGGER = get_logger() -def sign_with_signxml(private_key, data, cert, reference_uri=None): +def sign_with_signxml(data: str, provider: "SAMLProvider", reference_uri=None) -> str: """Sign Data with signxml""" key = serialization.load_pem_private_key( - str.encode("\n".join([x.strip() for x in private_key.split("\n")])), + str.encode("\n".join([x.strip() for x in provider.signing_key.split("\n")])), password=None, backend=default_backend(), ) # defused XML is not used here because it messes up XML namespaces # Data is trusted, so lxml is ok root = etree.fromstring(data) # nosec - signer = XMLSigner(c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#") - signed = signer.sign(root, key=key, cert=[cert], reference_uri=reference_uri) - XMLVerifier().verify(signed, x509_cert=cert) + signer = XMLSigner( + c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#", + signature_algorithm=provider.signature_algorithm, + digest_algorithm=provider.digest_algorithm, + ) + signed = signer.sign( + root, key=key, cert=[provider.signing_cert], reference_uri=reference_uri + ) + XMLVerifier().verify(signed, x509_cert=provider.signing_cert) return etree.tostring(signed).decode("utf-8") # nosec -def get_signature_xml(): +def get_signature_xml() -> str: """Returns XML Signature for subject.""" return render_to_string("saml/xml/signature.xml", {})