From a393097504074349d368b9cf9f59e7e43cfff8dc Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 11 Jul 2020 19:57:27 +0200 Subject: [PATCH] */saml: start implementing unittests, fix signing --- .../providers/saml/processors/assertion.py | 3 +- .../saml/processors/request_parser.py | 3 +- passbook/sources/saml/processors/request.py | 61 +++++++++++++++++-- passbook/sources/saml/views.py | 13 ++-- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/passbook/providers/saml/processors/assertion.py b/passbook/providers/saml/processors/assertion.py index ad263853b..ce3af0503 100644 --- a/passbook/providers/saml/processors/assertion.py +++ b/passbook/providers/saml/processors/assertion.py @@ -1,7 +1,6 @@ """SAML Assertion generator""" from types import GeneratorType -from defusedxml import ElementTree from django.http import HttpRequest from lxml import etree # nosec from lxml.etree import Element, SubElement # nosec @@ -212,4 +211,4 @@ class AssertionProcessor: signed, x509_cert=self.provider.signing_kp.certificate_data ) return etree.tostring(signed).decode("utf-8") # nosec - return ElementTree.tostring(root_response).decode() + return etree.tostring(root_response).decode("utf-8") # nosec diff --git a/passbook/providers/saml/processors/request_parser.py b/passbook/providers/saml/processors/request_parser.py index 95daedbff..099f78f15 100644 --- a/passbook/providers/saml/processors/request_parser.py +++ b/passbook/providers/saml/processors/request_parser.py @@ -1,6 +1,6 @@ """SAML AuthNRequest Parser and dataclass""" -from typing import Optional from dataclasses import dataclass +from typing import Optional from cryptography.exceptions import InvalidSignature from defusedxml import ElementTree @@ -9,7 +9,6 @@ from structlog import get_logger from passbook.providers.saml.exceptions import CannotHandleAssertion from passbook.providers.saml.models import SAMLProvider -from passbook.providers.saml.utils import get_random_id from passbook.providers.saml.utils.encoding import decode_base64_and_inflate LOGGER = get_logger() diff --git a/passbook/sources/saml/processors/request.py b/passbook/sources/saml/processors/request.py index e0983cd71..b2ce814bf 100644 --- a/passbook/sources/saml/processors/request.py +++ b/passbook/sources/saml/processors/request.py @@ -1,15 +1,20 @@ """SAML AuthnRequest Processor""" -from defusedxml import ElementTree +from typing import Dict + from django.http import HttpRequest +from lxml import etree # nosec from lxml.etree import Element # nosec +from signxml import XMLSigner, methods from passbook.providers.saml.utils import get_random_id +from passbook.providers.saml.utils.encoding import deflate_and_base64_encode from passbook.providers.saml.utils.time import get_time_string from passbook.sources.saml.models import SAMLSource from passbook.sources.saml.processors.constants import ( NS_MAP, NS_SAML_ASSERTION, NS_SAML_PROTOCOL, + NS_SIGNATURE, ) @@ -19,9 +24,14 @@ class RequestProcessor: source: SAMLSource http_request: HttpRequest + request_id: str + issue_instant: str + def __init__(self, source: SAMLSource, request: HttpRequest): self.source = source self.http_request = request + self.request_id = get_random_id() + self.issue_instant = get_time_string() def get_issuer(self) -> Element: """Get Issuer Element""" @@ -35,19 +45,60 @@ class RequestProcessor: name_id_policy.text = self.source.name_id_policy return name_id_policy - def build_auth_n(self) -> str: + def get_auth_n(self) -> Element: """Get full AuthnRequest""" auth_n_request = Element(f"{{{NS_SAML_PROTOCOL}}}AuthnRequest", nsmap=NS_MAP) auth_n_request.attrib[ "AssertionConsumerServiceURL" ] = self.source.build_full_url(self.http_request) auth_n_request.attrib["Destination"] = self.source.sso_url - auth_n_request.attrib["ID"] = get_random_id() - auth_n_request.attrib["IssueInstant"] = get_time_string() + auth_n_request.attrib["ID"] = self.request_id + auth_n_request.attrib["IssueInstant"] = self.issue_instant auth_n_request.attrib["ProtocolBinding"] = self.source.binding_type auth_n_request.attrib["Version"] = "2.0" # Create issuer object auth_n_request.append(self.get_issuer()) # Create NameID Policy Object auth_n_request.append(self.get_name_id_policy()) - return ElementTree.tostring(auth_n_request).decode() + return auth_n_request + + def build_auth_n(self) -> str: + """Get Signed string representation of AuthN Request + (used for POST Bindings)""" + auth_n_request = self.get_auth_n() + + if self.source.signing_kp: + signed_request = XMLSigner().sign( + auth_n_request, + cert=self.source.signing_kp.certificate_data, + key=self.source.signing_kp.key_data, + ) + return etree.tostring(signed_request).decode() + + return etree.tostring(auth_n_request).decode() + + def build_auth_n_detached(self) -> Dict[str, str]: + """Get Dict AuthN Request for Redirect bindings, with detached + Signature""" + auth_n_request = self.get_auth_n() + + response_dict = { + "SAMLRequest": deflate_and_base64_encode( + etree.tostring(auth_n_request).decode() + ), + } + + if self.source.signing_kp: + signer = XMLSigner(methods.detached) + signature = signer.sign( + auth_n_request, + cert=self.source.signing_kp.certificate_data, + key=self.source.signing_kp.key_data, + ) + signature_value = signature.find( + f".//{{{NS_SIGNATURE}}}SignatureValue" + ).text + response_dict["Signature"] = signature_value + response_dict["SigAlg"] = signer.sign_alg + + return response_dict diff --git a/passbook/sources/saml/views.py b/passbook/sources/saml/views.py index 714e4c667..ce7b5b97c 100644 --- a/passbook/sources/saml/views.py +++ b/passbook/sources/saml/views.py @@ -11,7 +11,7 @@ from django.views.decorators.csrf import csrf_exempt from signxml import InvalidSignature from passbook.lib.views import bad_request_message -from passbook.providers.saml.utils.encoding import deflate_and_base64_encode, nice64 +from passbook.providers.saml.utils.encoding import nice64 from passbook.sources.saml.exceptions import ( MissingSAMLResponse, UnsupportedNameIDFormat, @@ -32,16 +32,15 @@ class InitiateView(View): raise Http404 relay_state = request.GET.get("next", "") request.session["sso_destination"] = relay_state - auth_n_req = RequestProcessor(source, request).build_auth_n() + auth_n_req = RequestProcessor(source, request) # If the source is configured for Redirect bindings, we can just redirect there if source.binding_type == SAMLBindingTypes.Redirect: - saml_request = deflate_and_base64_encode(auth_n_req) - url_args = urlencode( - {"SAMLRequest": saml_request, "RelayState": relay_state} - ) + url_params = auth_n_req.build_auth_n_detached() + url_params["RelayState"] = relay_state + url_args = urlencode(url_params) return redirect(f"{source.sso_url}?{url_args}") # As POST Binding we show a form - saml_request = nice64(auth_n_req) + saml_request = nice64(auth_n_req.build_auth_n()) if source.binding_type == SAMLBindingTypes.POST: return render( request,