def get_metadata(url): """ Gets the metadata XML from the provided URL :param url: Url where the XML of the Identity Provider Metadata is published. :type url: string :returns: metadata XML :rtype: string """ valid = False response = urllib2.urlopen(url) xml = response.read() if xml: try: dom = OneLogin_Saml2_XML.to_etree(xml) idp_descriptor_nodes = OneLogin_Saml2_XML.query(dom, '//md:IDPSSODescriptor') if idp_descriptor_nodes: valid = True except: pass if not valid: raise Exception('Not valid IdP XML found from URL: %s' % (url)) return xml
def get_status(dom): """ Gets Status from a Response. :param dom: The Response as XML :type: Document :returns: The Status, an array with the code and a message. :rtype: dict """ status = {} status_entry = OneLogin_Saml2_XML.query(dom, '/samlp:Response/samlp:Status') if len(status_entry) != 1: raise Exception('Missing valid Status on response') code_entry = OneLogin_Saml2_XML.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0]) if len(code_entry) != 1: raise Exception('Missing valid Status Code on response') code = code_entry[0].values()[0] status['code'] = code status['msg'] = '' message_entry = OneLogin_Saml2_XML.query(dom, '/samlp:Response/samlp:Status/samlp:StatusMessage', status_entry[0]) if len(message_entry) == 0: subcode_entry = OneLogin_Saml2_XML.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode', status_entry[0]) if len(subcode_entry) == 1: status['msg'] = subcode_entry[0].values()[0] elif len(message_entry) == 1: status['msg'] = message_entry[0].text return status
def add_x509_key_descriptors(metadata, cert=None): """ Adds the x509 descriptors (sign/encriptation) to the metadata The same cert will be used for sign/encrypt :param metadata: SAML Metadata XML :type metadata: string :param cert: x509 cert :type cert: string :returns: Metadata with KeyDescriptors :rtype: string """ if cert is None or cert == '': return metadata try: root = OneLogin_Saml2_XML.to_etree(metadata) except Exception as e: raise Exception('Error parsing metadata. ' + str(e)) assert root.tag == '{%s}EntityDescriptor' % OneLogin_Saml2_Constants.NS_MD try: sp_sso_descriptor = next(root.iterfind('.//md:SPSSODescriptor', namespaces=OneLogin_Saml2_Constants.NSMAP)) except StopIteration: raise Exception('Malformed metadata.') OneLogin_Saml2_Metadata.__add_x509_key_descriptors(sp_sso_descriptor, cert, False) OneLogin_Saml2_Metadata.__add_x509_key_descriptors(sp_sso_descriptor, cert, True) return OneLogin_Saml2_XML.to_string(root)
def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None): """ Generates a nameID. :param value: fingerprint :type: string :param sp_nq: SP Name Qualifier :type: string :param sp_format: SP Format :type: string :param cert: IdP Public Cert to encrypt the nameID :type: string :param debug: Activate the xmlsec debug :type: bool :returns: DOMElement | XMLSec nameID :rtype: string :param nq: IDP Name Qualifier :type: string """ root = OneLogin_Saml2_XML.make_root("{%s}container" % OneLogin_Saml2_Constants.NS_SAML) name_id = OneLogin_Saml2_XML.make_child(root, '{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML) if sp_nq is not None: name_id.set('SPNameQualifier', sp_nq) name_id.set('Format', sp_format) if nq is not None: name_id.set('NameQualifier', nq) name_id.text = value if cert is not None: xmlsec.enable_debug_trace(debug) # Load the public cert manager = xmlsec.KeysManager() manager.add_key(xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None)) # Prepare for encryption enc_data = xmlsec.template.encrypted_data_create( root, xmlsec.Transform.AES128, type=xmlsec.EncryptionType.ELEMENT, ns="xenc") xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns="dsig") enc_key = xmlsec.template.add_encrypted_key(key_info, xmlsec.Transform.RSA_OAEP) xmlsec.template.encrypted_data_ensure_cipher_value(enc_key) # Encrypt! enc_ctx = xmlsec.EncryptionContext(manager) enc_ctx.key = xmlsec.Key.generate(xmlsec.KeyData.AES, 128, xmlsec.KeyDataType.SESSION) enc_data = enc_ctx.encrypt_xml(enc_data, name_id) return '<saml:EncryptedID>' + compat.to_string(OneLogin_Saml2_XML.to_string(enc_data)) + '</saml:EncryptedID>' else: return OneLogin_Saml2_XML.extract_tag_text(root, "saml:NameID")
def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): """ Validates a signature node. :param signature_node: The signature node :type: Node :param xml: The element we should validate :type: Document :param cert: The public cert :type: string :param fingerprint: The fingerprint of the public cert :type: string :param fingerprintalg: The algorithm used to build the fingerprint :type: string :param validatecert: If true, will verify the signature and if the cert is valid. :type: bool :param debug: Activate the xmlsec debug :type: bool """ try: if (cert is None or cert == '') and fingerprint: x509_certificate_nodes = OneLogin_Saml2_XML.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate') if len(x509_certificate_nodes) > 0: x509_certificate_node = x509_certificate_nodes[0] x509_cert_value = x509_certificate_node.text x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg) if fingerprint == x509_fingerprint_value: cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) if cert is None or cert == '': return False # Check if Reference URI is empty reference_elem = OneLogin_Saml2_XML.query(signature_node, '//ds:Reference') if len(reference_elem) > 0: if reference_elem[0].get('URI') == '': reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID')) if validatecert: manager = xmlsec.KeysManager() manager.load_cert_from_memory(cert, xmlsec.KeyFormat.CERT_PEM, xmlsec.KeyDataType.TRUSTED) dsig_ctx = xmlsec.SignatureContext(manager) else: dsig_ctx = xmlsec.SignatureContext() dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) dsig_ctx.set_enabled_key_data([xmlsec.KeyData.X509]) dsig_ctx.verify(signature_node) return True except xmlsec.Error as e: if debug: print(e)
def validate_metadata_sign( xml, cert=None, fingerprint=None, fingerprintalg="sha1", validatecert=False, debug=False ): """ Validates a signature of a EntityDescriptor. :param xml: The element we should validate :type: string | Document :param cert: The pubic cert :type: string :param fingerprint: The fingerprint of the public cert :type: string :param fingerprintalg: The algorithm used to build the fingerprint :type: string :param validatecert: If true, will verify the signature and if the cert is valid. :type: bool :param debug: Activate the xmlsec debug :type: bool """ try: if xml is None or xml == "": raise Exception("Empty string supplied as input") elem = OneLogin_Saml2_XML.to_etree(xml) xmlsec.enable_debug_trace(debug) xmlsec.tree.add_ids(elem, ["ID"]) signature_nodes = OneLogin_Saml2_XML.query(elem, "/md:EntitiesDescriptor/ds:Signature") if len(signature_nodes) == 0: signature_nodes += OneLogin_Saml2_XML.query(elem, "/md:EntityDescriptor/ds:Signature") if len(signature_nodes) == 0: signature_nodes += OneLogin_Saml2_XML.query( elem, "/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature" ) signature_nodes += OneLogin_Saml2_XML.query( elem, "/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature" ) if len(signature_nodes) > 0: for signature_node in signature_nodes: if not OneLogin_Saml2_Utils.validate_node_sign( signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug ): return False return True else: return False except Exception: return False
def validate_num_assertions(self): """ Verifies that the document only contains a single Assertion (encrypted or not) :returns: True if only 1 assertion encrypted or not :rtype: bool """ encrypted_assertion_nodes = OneLogin_Saml2_XML.query(self.document, '//saml:EncryptedAssertion') assertion_nodes = OneLogin_Saml2_XML.query(self.document, '//saml:Assertion') return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1
def __add_x509_key_descriptors(root, cert, signing): key_descriptor = OneLogin_Saml2_XML.make_child(root, '{%s}KeyDescriptor' % OneLogin_Saml2_Constants.NS_MD) root.remove(key_descriptor) root.insert(0, key_descriptor) key_info = OneLogin_Saml2_XML.make_child(key_descriptor, '{%s}KeyInfo' % OneLogin_Saml2_Constants.NS_DS) key_data = OneLogin_Saml2_XML.make_child(key_info, '{%s}X509Data' % OneLogin_Saml2_Constants.NS_DS) x509_certificate = OneLogin_Saml2_XML.make_child(key_data, '{%s}X509Certificate' % OneLogin_Saml2_Constants.NS_DS) x509_certificate.text = OneLogin_Saml2_Utils.format_cert(cert, False) key_descriptor.set('use', ('encryption', 'signing')[signing])
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None): """ Validates a signature (Message or Assertion). :param xml: The element we should validate :type: string | Document :param cert: The public cert :type: string :param fingerprint: The fingerprint of the public cert :type: string :param fingerprintalg: The algorithm used to build the fingerprint :type: string :param validatecert: If true, will verify the signature and if the cert is valid. :type: bool :param debug: Activate the xmlsec debug :type: bool :param xpath: The xpath of the signed element :type: string """ try: if xml is None or xml == '': raise Exception('Empty string supplied as input') elem = OneLogin_Saml2_XML.to_etree(xml) xmlsec.enable_debug_trace(debug) xmlsec.tree.add_ids(elem, ["ID"]) if xpath: signature_nodes = OneLogin_Saml2_XML.query(elem, xpath) else: signature_nodes = OneLogin_Saml2_XML.query(elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH) if len(signature_nodes) == 0: signature_nodes = OneLogin_Saml2_XML.query(elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH) if len(signature_nodes) == 1: signature_node = signature_nodes[0] return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug) else: return False except xmlsec.Error as e: if debug: print(e) return False
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg="sha1", validatecert=False, debug=False): """ Validates a signature (Message or Assertion). :param xml: The element we should validate :type: string | Document :param cert: The pubic cert :type: string :param fingerprint: The fingerprint of the public cert :type: string :param fingerprintalg: The algorithm used to build the fingerprint :type: string :param validatecert: If true, will verify the signature and if the cert is valid. :type: bool :param debug: Activate the xmlsec debug :type: bool """ try: if xml is None or xml == "": raise Exception("Empty string supplied as input") elem = OneLogin_Saml2_XML.to_etree(xml) xmlsec.enable_debug_trace(debug) xmlsec.tree.add_ids(elem, ["ID"]) signature_nodes = OneLogin_Saml2_XML.query(elem, "/samlp:Response/ds:Signature") if not len(signature_nodes) > 0: signature_nodes += OneLogin_Saml2_XML.query( elem, "/samlp:Response/saml:EncryptedAssertion/saml:Assertion/ds:Signature" ) signature_nodes += OneLogin_Saml2_XML.query(elem, "/samlp:Response/saml:Assertion/ds:Signature") if len(signature_nodes) == 1: signature_node = signature_nodes[0] return OneLogin_Saml2_Utils.validate_node_sign( signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug ) else: return False except xmlsec.Error as e: if debug: print(e) return False
def get_issuer(request): """ Gets the Issuer of the Logout Request Message :param request: Logout Request Message :type request: string|DOMDocument :return: The Issuer :rtype: string """ elem = OneLogin_Saml2_XML.to_etree(request) issuer = None issuer_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:Issuer') if len(issuer_nodes) == 1: issuer = issuer_nodes[0].text return issuer
def get_session_indexes(request): """ Gets the SessionIndexes from the Logout Request :param request: Logout Request Message :type request: string|DOMDocument :return: The SessionIndex value :rtype: list """ elem = OneLogin_Saml2_XML.to_etree(request) session_indexes = [] session_index_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex') for session_index_node in session_index_nodes: session_indexes.append(session_index_node.text) return session_indexes
def validate_signed_elements(self, signed_elements): """ Verifies that the document has the expected signed nodes. """ if len(signed_elements) > 2: return False response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML if (response_tag in signed_elements and signed_elements.count(response_tag) > 1) or \ (assertion_tag in signed_elements and signed_elements.count(assertion_tag) > 1) or \ (response_tag not in signed_elements and assertion_tag not in signed_elements): return False # Check that the signed elements found here, are the ones that will be verified # by OneLogin_Saml2_Utils.validate_sign if response_tag in signed_elements: expected_signature_nodes = OneLogin_Saml2_XML.query(self.document, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH) if len(expected_signature_nodes) != 1: raise Exception('Unexpected number of Response signatures found. SAML Response rejected.') if assertion_tag in signed_elements: expected_signature_nodes = self.__query(OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH) if len(expected_signature_nodes) != 1: raise Exception('Unexpected number of Assertion signatures found. SAML Response rejected.') return True
def __query(self, query): """ Extracts a node from the Etree (Logout Response Message) :param query: Xpath Expression :type query: string :return: The queried node :rtype: Element """ return OneLogin_Saml2_XML.query(self.document, query)
def __decrypt_assertion(self, xml): """ Decrypts the Assertion :raises: Exception if no private key available :param xml: Encrypted Assertion :type xml: Element :returns: Decrypted Assertion :rtype: Element """ key = self.__settings.get_sp_key() debug = self.__settings.is_debug_active() if not key: raise Exception('No private key available, check settings') encrypted_assertion_nodes = OneLogin_Saml2_XML.query(xml, '/samlp:Response/saml:EncryptedAssertion') if encrypted_assertion_nodes: encrypted_data_nodes = OneLogin_Saml2_XML.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData') if encrypted_data_nodes: keyinfo = OneLogin_Saml2_XML.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo') if not keyinfo: raise Exception('No KeyInfo present, invalid Assertion') keyinfo = keyinfo[0] children = keyinfo.getchildren() if not children: raise Exception('No child to KeyInfo, invalid Assertion') for child in children: if 'RetrievalMethod' in child.tag: if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': raise Exception('Unsupported Retrieval Method found') uri = child.attrib['URI'] if not uri.startswith('#'): break uri = uri.split('#')[1] encrypted_key = OneLogin_Saml2_XML.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id="' + uri + '"]') if encrypted_key: keyinfo.append(encrypted_key[0]) encrypted_data = encrypted_data_nodes[0] decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug) xml.replace(encrypted_assertion_nodes[0], decrypted) return xml
def testGetID(self): """ Tests the get_id method of the OneLogin_Saml2_Authn_Request. """ saml_settings = self.loadSettingsJSON() settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) document = OneLogin_Saml2_XML.to_etree(inflated) self.assertEqual(authn_request.get_id(), document.get('ID', None))
def get_id(request): """ Returns the ID of the Logout Request :param request: Logout Request Message :type request: string|DOMDocument :return: string ID :rtype: str object """ elem = OneLogin_Saml2_XML.to_etree(request) return elem.get('ID', None)
def get_nameid_data(request, key=None): """ Gets the NameID Data of the the Logout Request :param request: Logout Request Message :type request: string|DOMDocument :param key: The SP key :type key: string :return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) :rtype: dict """ elem = OneLogin_Saml2_XML.to_etree(request) name_id = None encrypted_entries = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:EncryptedID') if len(encrypted_entries) == 1: if key is None: raise Exception('Key is required in order to decrypt the NameID') encrypted_data_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_data_nodes) == 1: encrypted_data = encrypted_data_nodes[0] name_id = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) else: entries = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:NameID') if len(entries) == 1: name_id = entries[0] if name_id is None: raise Exception('Not NameID found in the Logout Request') name_id_data = { 'Value': name_id.text } for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: if attr in name_id.attrib: name_id_data[attr] = name_id.attrib[attr] return name_id_data
def process_signed_elements(self): """ Verifies the signature nodes: - Checks that are Response or Assertion - Check that IDs and reference URI are unique and consistent. :returns: The signed elements tag names :rtype: list """ sign_nodes = self.__query('//ds:Signature') signed_elements = [] verified_seis = [] verified_ids = [] response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML for sign_node in sign_nodes: signed_element = sign_node.getparent().tag if signed_element != response_tag and signed_element != assertion_tag: raise Exception('Invalid Signature Element %s SAML Response rejected' % signed_element) if not sign_node.getparent().get('ID'): raise Exception('Signed Element must contain an ID. SAML Response rejected') id_value = sign_node.getparent().get('ID') if id_value in verified_ids: raise Exception('Duplicated ID. SAML Response rejected') verified_ids.append(id_value) # Check that reference URI matches the parent ID and no duplicate References or IDs ref = OneLogin_Saml2_XML.query(sign_node, './/ds:Reference') if ref: ref = ref[0] if ref.get('URI'): sei = ref.get('URI')[1:] if sei != id_value: raise Exception('Found an invalid Signed Element. SAML Response rejected') if sei in verified_seis: raise Exception('Duplicated Reference URI. SAML Response rejected') verified_seis.append(sei) signed_elements.append(signed_element) if signed_elements: if not self.validate_signed_elements(signed_elements): raise Exception('Found an unexpected Signature Element. SAML Response rejected') return signed_elements
def get_status(dom): """ Gets Status from a Response. :param dom: The Response as XML :type: Document :returns: The Status, an array with the code and a message. :rtype: dict """ status = {} status_entry = OneLogin_Saml2_XML.query(dom, "/samlp:Response/samlp:Status") if len(status_entry) == 0: raise Exception("Missing Status on response") code_entry = OneLogin_Saml2_XML.query(dom, "/samlp:Response/samlp:Status/samlp:StatusCode", status_entry[0]) if len(code_entry) == 0: raise Exception("Missing Status Code on response") code = code_entry[0].values()[0] status["code"] = code message_entry = OneLogin_Saml2_XML.query( dom, "/samlp:Response/samlp:Status/samlp:StatusMessage", status_entry[0] ) if len(message_entry) == 0: subcode_entry = OneLogin_Saml2_XML.query( dom, "/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode", status_entry[0] ) if len(subcode_entry) > 0: status["msg"] = subcode_entry[0].values()[0] else: status["msg"] = "" else: status["msg"] = message_entry[0].text return status
def is_valid(self, request_data, request_id=None): """ Determines if the SAML LogoutResponse is valid :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string :return: Returns if the SAML LogoutResponse is or not valid :rtype: boolean """ self.__error = None try: idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = request_data['get_data'] if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') security = self.__settings.get_security_data() # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided in_response_to = self.document.get('InResponseTo', None) if request_id is not None and in_response_to and in_response_to != request_id: raise Exception('The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id)) # Check issuer issuer = self.get_issuer() if issuer is not None and issuer != idp_entity_id: raise Exception('Invalid issuer in the Logout Request') current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check destination destination = self.document.get('Destination', None) if destination and current_url not in destination: raise Exception('The LogoutRequest was received at $currentURL instead of $destination') if security['wantMessagesSigned']: if 'Signature' not in get_data: raise Exception('The Message of the Logout Response is not signed and the SP require it') return True # pylint: disable=R0801 except Exception as err: self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) return False
def __query(self, query): """ Extracts nodes that match the query from the Response :param query: Xpath Expresion :type query: String :returns: The queried nodes :rtype: list """ if self.encrypted: document = self.decrypted_document else: document = self.document return OneLogin_Saml2_XML.query(document, query)
def __init__(self, settings, response=None): """ Constructs a Logout Response object (Initialize params from settings and if provided load the Logout Response. Arguments are: * (OneLogin_Saml2_Settings) settings. Setting data * (string) response. An UUEncoded SAML Logout response from the IdP. """ self.__settings = settings self.__error = None if response is not None: self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response) self.document = OneLogin_Saml2_XML.to_etree(self.__logout_response)
def get_issuers(self): """ Gets the issuers (from message and from assertion) :returns: The issuers :rtype: list """ issuers = set() message_issuer_nodes = OneLogin_Saml2_XML.query(self.document, '/samlp:Response/saml:Issuer') if len(message_issuer_nodes) == 1: issuers.add(message_issuer_nodes[0].text) else: raise Exception('Issuer of the Response not found or multiple.') assertion_issuer_nodes = self.__query_assertion('/saml:Issuer') if len(assertion_issuer_nodes) == 1: issuers.add(assertion_issuer_nodes[0].text) else: raise Exception('Issuer of the Assertion not found or multiple.') return list(set(issuers))
def validate_metadata(self, xml): """ Validates an XML SP Metadata. :param xml: Metadata's XML that will be validate :type xml: string :returns: The list of found errors :rtype: list """ assert isinstance(xml, compat.text_types) if len(xml) == 0: raise Exception('Empty string supplied as input') errors = [] root = OneLogin_Saml2_XML.validate_xml(xml, 'saml-schema-metadata-2.0.xsd', self.__debug) if isinstance(root, str): errors.append(root) else: if root.tag != '{%s}EntityDescriptor' % OneLogin_Saml2_Constants.NS_MD: errors.append('noEntityDescriptor_xml') else: if (len(root.findall('.//md:SPSSODescriptor', namespaces=OneLogin_Saml2_Constants.NSMAP))) != 1: errors.append('onlySPSSODescriptor_allowed_xml') else: valid_until, cache_duration = root.get('validUntil'), root.get('cacheDuration') if valid_until: valid_until = OneLogin_Saml2_Utils.parse_SAML_to_time(valid_until) expire_time = OneLogin_Saml2_Utils.get_expire_time(cache_duration, valid_until) if expire_time is not None and int(time()) > int(expire_time): errors.append('expired_xml') # TODO: Validate Sign return errors
def __init__(self, settings, response): """ Constructs the response object. :param settings: The setting info :type settings: OneLogin_Saml2_Setting object :param response: The base64 encoded, XML string containing the samlp:Response :type response: string """ self.__settings = settings self.__error = None self.response = OneLogin_Saml2_Utils.b64decode(response) self.document = OneLogin_Saml2_XML.to_etree(self.response) self.decrypted_document = None self.encrypted = None # Quick check for the presence of EncryptedAssertion encrypted_assertion_nodes = self.__query('/samlp:Response/saml:EncryptedAssertion') if encrypted_assertion_nodes: decrypted_document = deepcopy(self.document) self.encrypted = True self.decrypted_document = self.__decrypt_assertion(decrypted_document)
def decrypt_element(encrypted_data, key, debug=False): """ Decrypts an encrypted element. :param encrypted_data: The encrypted data. :type: lxml.etree.Element | DOMElement | basestring :param key: The key. :type: string :param debug: Activate the xmlsec debug :type: bool :returns: The decrypted element. :rtype: lxml.etree.Element """ encrypted_data = OneLogin_Saml2_XML.to_etree(encrypted_data) xmlsec.enable_debug_trace(debug) manager = xmlsec.KeysManager() manager.add_key(xmlsec.Key.from_memory(key, xmlsec.KeyFormat.PEM, None)) enc_ctx = xmlsec.EncryptionContext(manager) return enc_ctx.decrypt(encrypted_data)
def is_valid(self, request_data): """ Checks if the Logout Request recieved is valid :param request_data: Request Data :type request_data: dict :return: If the Logout Request is or not valid :rtype: boolean """ self.__error = None try: root = OneLogin_Saml2_XML.to_etree(self.__logout_request) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = ('get_data' in request_data and request_data['get_data']) or dict() if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml(root, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check NotOnOrAfter if root.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): raise Exception('Timing issues (please check your clock settings)') # Check destination if root.get('Destination', None): destination = root.get('Destination') if destination != '': if current_url not in destination: raise Exception( 'The LogoutRequest was received at ' '%(currentURL)s instead of %(destination)s' % { 'currentURL': current_url, 'destination': destination, } ) # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(root) if issuer is not None and issuer != idp_entity_id: raise Exception('Invalid issuer in the Logout Request') if security['wantMessagesSigned']: if 'Signature' not in get_data: raise Exception('The Message of the Logout Request is not signed and the SP require it') return True except Exception as err: # pylint: disable=R0801 self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) return False
def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Adds signature key and senders certificate to an element (Message or Assertion). :param xml: The element we should sign :type: string | Document :param key: The private key :type: string :param cert: The public :type: string :param debug: Activate the xmlsec debug :type: bool :param sign_algorithm: Signature algorithm method :type sign_algorithm: string """ if xml is None or xml == '': raise Exception('Empty string supplied as input') elem = OneLogin_Saml2_XML.to_etree(xml) xmlsec.enable_debug_trace(debug) xmlsec.tree.add_ids(elem, ["ID"]) # Sign the metadata with our private key. sign_algorithm_transform_map = { OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.Transform.DSA_SHA1, OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.Transform.RSA_SHA1, OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.Transform.RSA_SHA256, OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.Transform.RSA_SHA384, OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.Transform.RSA_SHA512 } sign_algorithm_transform = sign_algorithm_transform_map.get(sign_algorithm, xmlsec.Transform.RSA_SHA1) signature = xmlsec.template.create(elem, xmlsec.Transform.EXCL_C14N, sign_algorithm_transform, ns='ds') issuer = OneLogin_Saml2_XML.query(elem, '//saml:Issuer') if len(issuer) > 0: issuer = issuer[0] issuer.addnext(signature) else: elem[0].insert(0, signature) elem_id = elem.get('ID', None) if elem_id: elem_id = '#' + elem_id ref = xmlsec.template.add_reference(signature, xmlsec.Transform.SHA1, uri=elem_id) xmlsec.template.add_transform(ref, xmlsec.Transform.ENVELOPED) xmlsec.template.add_transform(ref, xmlsec.Transform.EXCL_C14N) key_info = xmlsec.template.ensure_key_info(signature) xmlsec.template.add_x509_data(key_info) dsig_ctx = xmlsec.SignatureContext() sign_key = xmlsec.Key.from_memory(key, xmlsec.KeyFormat.PEM, None) sign_key.load_cert_from_memory(cert, xmlsec.KeyFormat.PEM) dsig_ctx.key = sign_key dsig_ctx.sign(signature) return OneLogin_Saml2_XML.to_string(elem)
def is_valid(self, request_data, request_id=None): """ Validates the response object. :param request_data: Request Data :type request_data: dict :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP :type request_id: string :returns: True if the SAML Response is valid, False if not :rtype: bool """ self.__error = None try: # Checks SAML version if self.document.get('Version', None) != '2.0': raise Exception('Unsupported SAML version') # Checks that ID exists if self.document.get('ID', None) is None: raise Exception('Missing ID attribute on SAML Response') # Checks that the response only has one assertion if not self.validate_num_assertions(): raise Exception('SAML Response must contain 1 assertion') # Checks that the response has the SUCCESS status self.check_status() idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data['entityId'] sign_nodes = self.__query('//ds:Signature') signed_elements = [] for sign_node in sign_nodes: signed_elements.append(sign_node.getparent().tag) if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise Exception('Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd') security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided in_response_to = self.document.get('InResponseTo', None) if in_response_to and request_id: if in_response_to != request_id: raise Exception('The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id)) if not self.encrypted and security['wantAssertionsEncrypted']: raise Exception('The assertion of the Response is not encrypted and the SP require it') if security['wantNameIdEncrypted']: encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_nameid_nodes) == 0: raise Exception('The NameID of the Response is not encrypted and the SP require it') # Checks that there is at least one AttributeStatement if required attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement') if security['wantAttributeStatement'] and not attribute_statement_nodes: raise Exception('There is no AttributeStatement on the Response') # Validates Assertion timestamps if not self.validate_timestamps(): raise Exception('Timing issues (please check your clock settings)') encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute') if encrypted_attributes_nodes: raise Exception('There is an EncryptedAttribute in the Response and this SP not support them') # Checks destination destination = self.document.get('Destination', '') if destination: if not destination.startswith(current_url): # TODO: Review if following lines are required, since we can control the # request_data # current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data) # if not destination.startswith(current_url_routed): raise Exception('The response was received at %s instead of %s' % (current_url, destination)) # Checks audience valid_audiences = self.get_audiences() if valid_audiences and sp_entity_id not in valid_audiences: raise Exception('%s is not a valid audience for this Response' % sp_entity_id) # Checks the issuers issuers = self.get_issuers() for issuer in issuers: if issuer is None or issuer != idp_entity_id: raise Exception('Invalid issuer in the Assertion/Response') # Checks the session Expiration session_expiration = self.get_session_not_on_or_after() if session_expiration and session_expiration <= OneLogin_Saml2_Utils.now(): raise Exception('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response') # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid any_subject_confirmation = False subject_confirmation_nodes = self.__query_assertion('/saml:Subject/saml:SubjectConfirmation') for scn in subject_confirmation_nodes: method = scn.get('Method', None) if method and method != OneLogin_Saml2_Constants.CM_BEARER: continue sc_data = scn.find('saml:SubjectConfirmationData', namespaces=OneLogin_Saml2_Constants.NSMAP) if sc_data is None: continue else: irt = sc_data.get('InResponseTo', None) if in_response_to and irt and irt != in_response_to: continue recipient = sc_data.get('Recipient', None) if recipient and current_url not in recipient: continue nooa = sc_data.get('NotOnOrAfter', None) if nooa: parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa) if parsed_nooa <= OneLogin_Saml2_Utils.now(): continue nb = sc_data.get('NotBefore', None) if nb: parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(nb) if parsed_nb > OneLogin_Saml2_Utils.now(): continue any_subject_confirmation = True break if not any_subject_confirmation: raise Exception('A valid SubjectConfirmation was not found on this Response') if security['wantAssertionsSigned'] and ('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML) not in signed_elements: raise Exception('The Assertion of the Response is not signed and the SP require it') if security['wantMessagesSigned'] and ('{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP) not in signed_elements: raise Exception('The Message of the Response is not signed and the SP require it') if len(signed_elements) > 0: if len(signed_elements) > 2: raise Exception('Too many Signatures found. SAML Response rejected') cert = idp_data['x509cert'] fingerprint = idp_data['certFingerprint'] fingerprintalg = idp_data['certFingerprintAlgorithm'] # If find a Signature on the Response, validates it checking the original response if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements: document_to_validate = self.document # Otherwise validates the assertion (decrypted assertion if was encrypted) else: if self.encrypted: document_to_validate = self.decrypted_document else: document_to_validate = self.document if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg): raise Exception('Signature validation failed. SAML Response rejected') else: raise Exception('No Signature found. SAML Response rejected') return True except Exception as err: self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) return False