From eabe609a52c8d42a5ed2b68b72d1fc1976320709 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 26 Jun 2014 04:29:25 +0200 Subject: [PATCH] Add fingerprint support, check local IdP cert with xml cert --- example.py | 10 +++- onelogin/saml/Response.py | 1 + onelogin/saml/SignatureVerifier.py | 25 +++++----- onelogin/saml/Utils.py | 79 ++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 onelogin/saml/Utils.py diff --git a/example.py b/example.py index 217cd36f..4261f6eb 100644 --- a/example.py +++ b/example.py @@ -10,6 +10,8 @@ from BaseHTTPServer import HTTPServer from onelogin.saml import AuthRequest, Response +from onelogin.saml.Utils import format_finger_print, calculate_x509_fingerprint + __version__ = '0.1' @@ -143,7 +145,13 @@ def main(config_file): cert_path = os.path.abspath(cert_path) with open(cert_path) as f: - settings['idp_cert_fingerprint'] = f.read() + cert = f.read() + fingerprint = calculate_x509_fingerprint(cert) + if fingerprint: + settings['idp_cert_fingerprint'] = fingerprint + else: + formated = format_finger_print(settings['idp_cert_fingerprint']) + settings['idp_cert_fingerprint'] = formated parts = urlparse.urlparse(settings['assertion_consumer_service_url']) SampleAppHTTPRequestHandler.protocol_version = 'HTTP/1.0' diff --git a/onelogin/saml/Response.py b/onelogin/saml/Response.py index 514015c6..77060e47 100644 --- a/onelogin/saml/Response.py +++ b/onelogin/saml/Response.py @@ -5,6 +5,7 @@ from onelogin.saml import SignatureVerifier + namespaces = dict( samlp='urn:oasis:names:tc:SAML:2.0:protocol', saml='urn:oasis:names:tc:SAML:2.0:assertion', diff --git a/onelogin/saml/SignatureVerifier.py b/onelogin/saml/SignatureVerifier.py index ee54fc50..de7d088d 100644 --- a/onelogin/saml/SignatureVerifier.py +++ b/onelogin/saml/SignatureVerifier.py @@ -6,6 +6,8 @@ from lxml import etree +from onelogin.saml.Utils import calculate_x509_fingerprint, format_cert + log = logging.getLogger(__name__) @@ -81,6 +83,14 @@ def verify(document, signature, _etree=None, _tempfile=None, _subprocess=None, if signatureNodes and signatureNodes[0].getparent().tag == '{urn:oasis:names:tc:SAML:2.0:protocol}Response': parent_id_container = 'urn:oasis:names:tc:SAML:2.0:protocol:Response' + certificateNodes = document.xpath("//ds:X509Certificate", namespaces={'ds': 'http://www.w3.org/2000/09/xmldsig#'}) + + if not certificateNodes or calculate_x509_fingerprint(certificateNodes[0].text) != signature: + return False + else: + # use the x509 cert instead of fingerprint required by xmlsec + signature = format_cert(certificateNodes[0].text) + xmlsec_bin = _get_xmlsec_bin() verified = False @@ -94,20 +104,7 @@ def verify(document, signature, _etree=None, _tempfile=None, _subprocess=None, xml_fp.write(doc_str) xml_fp.seek(0) with _tempfile.NamedTemporaryFile(delete=False) as cert_fp: - if signature.startswith( - '-----BEGIN CERTIFICATE-----' - ): - # If there's no matching 'END CERTIFICATE' - # cryptpAppKeyLoad will fail - cert_fp.write(signature) - else: - cert_fp.write( - '{begin}\n{signature}\n{end}'.format( - begin='-----BEGIN CERTIFICATE-----', - signature=signature, - end='-----END CERTIFICATE-----', - ) - ) + cert_fp.write(signature) cert_fp.seek(0) cert_filename = cert_fp.name diff --git a/onelogin/saml/Utils.py b/onelogin/saml/Utils.py new file mode 100644 index 00000000..456280cf --- /dev/null +++ b/onelogin/saml/Utils.py @@ -0,0 +1,79 @@ +import base64 +from hashlib import sha1 +from textwrap import wrap + + +def format_finger_print(fingerprint): + """ + Formates a fingerprint. + + :param fingerprint: fingerprint + :type: string + + :returns: Formated fingerprint + :rtype: string + """ + formated_fingerprint = fingerprint.replace(':', '') + return formated_fingerprint.lower() + + +def calculate_x509_fingerprint(x509_cert): + """ + Calculates the fingerprint of a x509cert. + + :param x509_cert: x509 cert + :type: string + + :returns: Formated fingerprint + :rtype: string + """ + assert isinstance(x509_cert, basestring) + + lines = x509_cert.split('\n') + data = '' + + for line in lines: + # Remove '\r' from end of line if present. + line = line.rstrip() + if line == '-----BEGIN CERTIFICATE-----': + # Delete junk from before the certificate. + data = '' + elif line == '-----END CERTIFICATE-----': + # Ignore data after the certificate. + break + elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----': + # This isn't an X509 certificate. + return None + else: + # Append the current line to the certificate data. + data += line + # "data" now contains the certificate as a base64-encoded string. The + # fingerprint of the certificate is the sha1-hash of the certificate. + return sha1(base64.b64decode(data)).hexdigest().lower() + + +def format_cert(cert, heads=True): + """ + Returns a x509 cert (adding header & footer if required). + + :param cert: A x509 unformated cert + :type: string + + :param heads: True if we want to include head and footer + :type: boolean + + :returns: Formated cert + :rtype: string + """ + x509_cert = cert.replace('\x0D', '') + x509_cert = x509_cert.replace('\r', '') + x509_cert = x509_cert.replace('\n', '') + if len(x509_cert) > 0: + x509_cert = x509_cert.replace('-----BEGIN CERTIFICATE-----', '') + x509_cert = x509_cert.replace('-----END CERTIFICATE-----', '') + x509_cert = x509_cert.replace(' ', '') + + if heads: + x509_cert = '-----BEGIN CERTIFICATE-----\n' + '\n'.join(wrap(x509_cert, 64)) + '\n-----END CERTIFICATE-----\n' + + return x509_cert