def testDecryptElementInplace(self): """ Tests the decrypt_element method of the OneLogin_Saml2_Utils with inplace=True """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) key = settings.get_sp_key() xml_nameid_enc = b64decode(self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64'))) dom = fromstring(xml_nameid_enc) encrypted_node = dom.xpath('//saml:EncryptedID/xenc:EncryptedData', namespaces=OneLogin_Saml2_Constants.NSMAP)[0] # can be decrypted twice when copy the node first for _ in range(2): decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=False) self.assertIn('NameID', decrypted_nameid.tag) self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text) # can only be decrypted once in place decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=True) self.assertIn('NameID', decrypted_nameid.tag) self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text) # can't be decrypted twice since it has been decrypted inplace with self.assertRaisesRegex(Exception, "(1, 'failed to decrypt')"): OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=True)
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 = b64decode(response) self.document = fromstring(self.response, forbid_dtd=True) self.decrypted_document = None self.encrypted = None self.valid_scd_not_on_or_after = 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 test_start_authentication(self, _, service_provider, identity_providers): configuration = create_autospec(spec=SAMLConfiguration) configuration.service_provider_debug_mode = MagicMock( return_value=False) configuration.service_provider_strict_mode = MagicMock( return_value=False) configuration.get_service_provider = MagicMock( return_value=service_provider) configuration.get_identity_providers = MagicMock( return_value=identity_providers) onelogin_configuration = SAMLOneLoginConfiguration(configuration) subject_parser = SAMLSubjectParser() parser = DSLParser() visitor = DSLEvaluationVisitor() evaluator = DSLEvaluator(parser, visitor) subject_filter = SAMLSubjectFilter(evaluator) authentication_manager = SAMLAuthenticationManager( onelogin_configuration, subject_parser, subject_filter) with self.app.test_request_context("/"): result = authentication_manager.start_authentication( self._db, fixtures.IDP_1_ENTITY_ID, "") query_items = parse_qs(urlsplit(result).query) saml_request = query_items["SAMLRequest"][0] decoded_saml_request = OneLogin_Saml2_Utils.decode_base64_and_inflate( saml_request) validation_result = OneLogin_Saml2_XML.validate_xml( decoded_saml_request, "saml-schema-protocol-2.0.xsd", False) assert isinstance(validation_result, OneLogin_Saml2_XML._element_class) saml_request_dom = fromstring(decoded_saml_request) acs_url = saml_request_dom.get("AssertionConsumerServiceURL") assert acs_url == SERVICE_PROVIDER_WITH_UNSIGNED_REQUESTS.acs_service.url acs_binding = saml_request_dom.get("ProtocolBinding") assert (acs_binding == SERVICE_PROVIDER_WITH_UNSIGNED_REQUESTS. acs_service.binding.value) sso_url = saml_request_dom.get("Destination") assert sso_url == IDENTITY_PROVIDERS[0].sso_service.url name_id_policy_nodes = OneLogin_Saml2_XML.query( saml_request_dom, "./samlp:NameIDPolicy") assert name_id_policy_nodes is not None assert len(name_id_policy_nodes) == 1 name_id_policy_node = name_id_policy_nodes[0] name_id_format = name_id_policy_node.get("Format") assert (name_id_format == SERVICE_PROVIDER_WITH_UNSIGNED_REQUESTS.name_id_format)
def __query(self, query): """ Extracts a node from the DOMDocument (Logout Response Menssage) :param query: Xpath Expresion :type query: string :return: The queried node :rtype: DOMNodeList """ # Switch to lxml for querying xml = self.document.toxml() return OneLogin_Saml2_Utils.query(fromstring(xml, forbid_dtd=True), query)
def decrypt_element(encrypted_data, key, debug=False, inplace=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 :param inplace: update passed data with decrypted result :type: bool :returns: The decrypted element. :rtype: lxml.etree.Element """ if isinstance(encrypted_data, Element): encrypted_data = fromstring(str(encrypted_data.toxml()), forbid_dtd=True) elif isinstance(encrypted_data, basestring): encrypted_data = fromstring(str(encrypted_data), forbid_dtd=True) elif not inplace and isinstance(encrypted_data, etree._Element): encrypted_data = deepcopy(encrypted_data) error_callback_method = None if debug: error_callback_method = print_xmlsec_errors xmlsec.set_error_callback(error_callback_method) mngr = xmlsec.KeysMngr() key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) mngr.addKey(key) enc_ctx = xmlsec.EncCtx(mngr) return enc_ctx.decrypt(encrypted_data)
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 """ if isinstance(request, etree._Element): elem = request else: if isinstance(request, Document): request = request.toxml() elem = fromstring(request, forbid_dtd=True) name_id = None encrypted_entries = OneLogin_Saml2_Utils.query( elem, '/samlp:LogoutRequest/saml:EncryptedID') if len(encrypted_entries) == 1: if key is None: raise OneLogin_Saml2_Error( 'Private Key is required in order to decrypt the NameID, check settings', OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND) encrypted_data_nodes = OneLogin_Saml2_Utils.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_Utils.query( elem, '/samlp:LogoutRequest/saml:NameID') if len(entries) == 1: name_id = entries[0] if name_id is None: raise OneLogin_Saml2_ValidationError( 'NameID not found in the Logout Request', OneLogin_Saml2_ValidationError.NO_NAMEID) name_id_data = {'Value': OneLogin_Saml2_Utils.element_text(name_id)} for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: if attr in name_id.attrib.keys(): name_id_data[attr] = name_id.attrib[attr] return name_id_data
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 """ if isinstance(request, etree._Element): elem = request else: if isinstance(request, Document): request = request.toxml() elem = fromstring(request, forbid_dtd=True) return elem.get('ID', None)
def validate(self, federation, metadata): """Verify that federated SAML metadata has not expired. :param federation: SAML federation :type federation: api.saml.metadata.federations.model.SAMLFederation :param metadata: SAML federation's aggregated metadata :type metadata: str :raises SAMLFederatedMetadataValidationError: in the case of validation errors """ self._logger.info( "Started validating the expiration time of the metadata belonging to {0}" .format(federation)) try: root = fromstring(metadata.encode("utf-8")) except Exception as exception: raise SAMLFederatedMetadataValidationError( "Metadata's XML is not valid", str(exception)) if "EntitiesDescriptor" not in root.tag: raise SAMLFederatedMetadataValidationError( 'Metadata\'s root element is not "EntitiesDescriptor"') valid_until = root.get("validUntil", None) if not valid_until: raise SAMLFederatedMetadataValidationError( 'Metadata does not contain "validUntil" attribute') valid_until = self._parse_saml_date_time(valid_until) now = utc_now() if valid_until < now and (now - valid_until) > self.MAX_CLOCK_SKEW: raise SAMLFederatedMetadataValidationError( "Metadata has already expired. " '"validUntil" is {0} while the current time is {1}'.format( valid_until, now)) if valid_until > now and (valid_until - now) > self.MAX_VALID_TIME: raise SAMLFederatedMetadataValidationError( "Expiration time is unexpectedly far into the future. " '"validUntil" is {0} while the current time is {1}'.format( valid_until, now)) self._logger.info( "Finished validating the expiration time of the metadata belonging to {0}" .format(federation))
def validate_xml(xml, schema, debug=False): """ Validates a xml against a schema :param xml: The xml that will be validated :type: string|DomDocument :param schema: The schema :type: string :param debug: If debug is active, the parse-errors will be showed :type: bool :returns: Error code or the DomDocument of the xml :rtype: string """ assert isinstance(xml, basestring) or isinstance( xml, Document) or isinstance(xml, etree._Element) assert isinstance(schema, basestring) if isinstance(xml, Document): xml = xml.toxml() elif isinstance(xml, etree._Element): xml = tostring(xml, encoding='unicode') # Switch to lxml for schema validation try: dom = fromstring(xml.encode('utf-8'), forbid_dtd=True) except Exception: return 'unloaded_xml' schema_file = join(dirname(__file__), 'schemas', schema) f_schema = open(schema_file, 'r') schema_doc = etree.parse(f_schema) f_schema.close() xmlschema = etree.XMLSchema(schema_doc) if not xmlschema.validate(dom): if debug: stderr.write('Errors validating the metadata') stderr.write(':\n\n') for error in xmlschema.error_log: stderr.write('%s\n' % error.message) return 'invalid_xml' return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True)
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 """ if isinstance(request, etree._Element): elem = request else: if isinstance(request, Document): request = request.toxml() elem = fromstring(request, forbid_dtd=True) issuer = None issuer_nodes = OneLogin_Saml2_Utils.query( elem, '/samlp:LogoutRequest/saml:Issuer') if len(issuer_nodes) == 1: issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0]) 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 """ if isinstance(request, etree._Element): elem = request else: if isinstance(request, Document): request = request.toxml() elem = fromstring(request, forbid_dtd=True) session_indexes = [] session_index_nodes = OneLogin_Saml2_Utils.query( elem, '/samlp:LogoutRequest/samlp:SessionIndex') for session_index_node in session_index_nodes: session_indexes.append( OneLogin_Saml2_Utils.element_text(session_index_node)) return session_indexes
def get_metadata(url, validate_cert=True): """ Gets the metadata XML from the provided URL :param url: Url where the XML of the Identity Provider Metadata is published. :type url: string :param validate_cert: If the url uses https schema, that flag enables or not the verification of the associated certificate. :type validate_cert: bool :returns: metadata XML :rtype: string """ valid = False if validate_cert: response = urllib2.urlopen(url) else: ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE response = urllib2.urlopen(url, context=ctx) xml = response.read() if xml: try: dom = fromstring(xml, forbid_dtd=True) idp_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:IDPSSODescriptor') if idp_descriptor_nodes: valid = True except Exception: pass if not valid: raise Exception('Not valid IdP XML found from URL: %s' % (url)) return xml
def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.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 :param digest_algorithm: Digest algorithm method :type digest_algorithm: string :returns: Signed XML :rtype: string """ if xml is None or xml == '': raise Exception('Empty string supplied as input') elif isinstance(xml, etree._Element): elem = xml elif isinstance(xml, Document): xml = xml.toxml() elem = fromstring(xml.encode('utf-8'), forbid_dtd=True) elif isinstance(xml, Element): xml.setAttributeNS(unicode(OneLogin_Saml2_Constants.NS_SAMLP), 'xmlns:samlp', unicode(OneLogin_Saml2_Constants.NS_SAMLP)) xml.setAttributeNS(unicode(OneLogin_Saml2_Constants.NS_SAML), 'xmlns:saml', unicode(OneLogin_Saml2_Constants.NS_SAML)) xml = xml.toxml() elem = fromstring(xml.encode('utf-8'), forbid_dtd=True) elif isinstance(xml, basestring): elem = fromstring(xml.encode('utf-8'), forbid_dtd=True) else: raise Exception('Error parsing xml string') error_callback_method = None if debug: error_callback_method = print_xmlsec_errors xmlsec.set_error_callback(error_callback_method) sign_algorithm_transform_map = { OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 } sign_algorithm_transform = sign_algorithm_transform_map.get( sign_algorithm, xmlsec.TransformRsaSha1) signature = Signature(xmlsec.TransformExclC14N, sign_algorithm_transform, nsPrefix='ds') issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer') if len(issuer) > 0: issuer = issuer[0] issuer.addnext(signature) elem_to_sign = issuer.getparent() else: entity_descriptor = OneLogin_Saml2_Utils.query( elem, '//md:EntityDescriptor') if len(entity_descriptor) > 0: elem.insert(0, signature) else: elem[0].insert(0, signature) elem_to_sign = elem elem_id = elem_to_sign.get('ID', None) if elem_id is not None: if elem_id: elem_id = '#' + elem_id else: generated_id = generated_id = OneLogin_Saml2_Utils.generate_unique_id( ) elem_id = '#' + generated_id elem_to_sign.attrib['ID'] = generated_id xmlsec.addIDs(elem_to_sign, ["ID"]) digest_algorithm_transform_map = { OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1, OneLogin_Saml2_Constants.SHA256: xmlsec.TransformSha256, OneLogin_Saml2_Constants.SHA384: xmlsec.TransformSha384, OneLogin_Saml2_Constants.SHA512: xmlsec.TransformSha512 } digest_algorithm_transform = digest_algorithm_transform_map.get( digest_algorithm, xmlsec.TransformSha1) ref = signature.addReference(digest_algorithm_transform) if elem_id: ref.attrib['URI'] = elem_id ref.addTransform(xmlsec.TransformEnveloped) ref.addTransform(xmlsec.TransformExclC14N) key_info = signature.ensureKeyInfo() key_info.addX509Data() dsig_ctx = xmlsec.DSigCtx() sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem) file_cert.close() dsig_ctx.signKey = sign_key dsig_ctx.sign(signature) return tostring(elem, encoding='unicode').encode('utf-8')
def is_valid(self, request_data, raise_exceptions=False): """ Checks if the Logout Request received is valid :param request_data: Request Data :type request_data: dict :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean :return: If the Logout Request is or not valid :rtype: boolean """ self.__error = None lowercase_urlencoding = False try: dom = fromstring(self.__logout_request, forbid_dtd=True) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] if 'get_data' in request_data.keys(): get_data = request_data['get_data'] else: get_data = {} if 'lowercase_urlencoding' in request_data.keys(): lowercase_urlencoding = request_data['lowercase_urlencoding'] if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml( dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): raise OneLogin_Saml2_ValidationError( 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd', OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT) security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query( request_data) # Check NotOnOrAfter if dom.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time( dom.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): raise OneLogin_Saml2_ValidationError( 'Could not validate timestamp: expired. Check system clock.', OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED) # Check destination destination = dom.get('Destination') if destination: if not OneLogin_Saml2_Utils.normalize_url( url=destination).startswith( OneLogin_Saml2_Utils.normalize_url( url=current_url)): raise Exception( 'The LogoutRequest was received at ' '%(currentURL)s instead of %(destination)s' % { 'currentURL': current_url, 'destination': destination, }, OneLogin_Saml2_ValidationError.WRONG_DESTINATION) # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom) if issuer is not None and issuer != idp_entity_id: raise OneLogin_Saml2_ValidationError( 'Invalid issuer in the Logout Request (expected %(idpEntityId)s, got %(issuer)s)' % { 'idpEntityId': idp_entity_id, 'issuer': issuer }, OneLogin_Saml2_ValidationError.WRONG_ISSUER) if security['wantMessagesSigned']: if 'Signature' not in get_data: raise OneLogin_Saml2_ValidationError( 'The Message of the Logout Request is not signed and the SP require it', OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE) if 'Signature' in get_data: if 'SigAlg' not in get_data: sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 else: sign_alg = get_data['SigAlg'] signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter( get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding) if 'RelayState' in get_data: signed_query = '%s&RelayState=%s' % ( signed_query, OneLogin_Saml2_Utils.get_encoded_parameter( get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) signed_query = '%s&SigAlg=%s' % ( signed_query, OneLogin_Saml2_Utils.get_encoded_parameter( get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) exists_x509cert = 'x509cert' in idp_data and idp_data[ 'x509cert'] exists_multix509sign = 'x509certMulti' in idp_data and \ 'signing' in idp_data['x509certMulti'] and \ idp_data['x509certMulti']['signing'] if not (exists_x509cert or exists_multix509sign): raise OneLogin_Saml2_Error( 'In order to validate the sign on the Logout Request, the x509cert of the IdP is required', OneLogin_Saml2_Error.CERT_NOT_FOUND) if exists_multix509sign: for cert in idp_data['x509certMulti']['signing']: if OneLogin_Saml2_Utils.validate_binary_sign( signed_query, b64decode(get_data['Signature']), cert, sign_alg): return True raise OneLogin_Saml2_ValidationError( 'Signature validation failed. Logout Request rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE) else: cert = idp_data['x509cert'] if not OneLogin_Saml2_Utils.validate_binary_sign( signed_query, b64decode(get_data['Signature']), cert, sign_alg): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. Logout Request rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE) return True except Exception as err: # pylint: disable=R0801sign_alg self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: print(err.__str__()) if raise_exceptions: raise err return False
def parse( idp_metadata, required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, entity_id=None): """ Parse the Identity Provider metadata and return a dict with extracted data. If there are multiple <IDPSSODescriptor> tags, parse only the first. Parse only those SSO endpoints with the same binding as given by the `required_sso_binding` parameter. Parse only those SLO endpoints with the same binding as given by the `required_slo_binding` parameter. If the metadata specifies multiple SSO endpoints with the required binding, extract only the first (the same holds true for SLO endpoints). :param idp_metadata: XML of the Identity Provider Metadata. :type idp_metadata: string :param required_sso_binding: Parse only POST or REDIRECT SSO endpoints. :type required_sso_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT or OneLogin_Saml2_Constants.BINDING_HTTP_POST :param required_slo_binding: Parse only POST or REDIRECT SLO endpoints. :type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT or OneLogin_Saml2_Constants.BINDING_HTTP_POST :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML that contains multiple EntityDescriptor. :type entity_id: string :returns: settings dict with extracted data :rtype: dict """ data = {} dom = fromstring(idp_metadata, forbid_dtd=True) entity_desc_path = '//md:EntityDescriptor' if entity_id: entity_desc_path += "[@entityID='%s']" % entity_id entity_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, entity_desc_path) idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = certs = None if len(entity_descriptor_nodes) > 0: entity_descriptor_node = entity_descriptor_nodes[0] idp_descriptor_nodes = OneLogin_Saml2_Utils.query(entity_descriptor_node, './md:IDPSSODescriptor') if len(idp_descriptor_nodes) > 0: idp_descriptor_node = idp_descriptor_nodes[0] idp_entity_id = entity_descriptor_node.get('entityID', None) want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None) name_id_format_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, './md:NameIDFormat') if len(name_id_format_nodes) > 0: idp_name_id_format = OneLogin_Saml2_Utils.element_text(name_id_format_nodes[0]) sso_nodes = OneLogin_Saml2_Utils.query( idp_descriptor_node, "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding ) if len(sso_nodes) > 0: idp_sso_url = sso_nodes[0].get('Location', None) slo_nodes = OneLogin_Saml2_Utils.query( idp_descriptor_node, "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding ) if len(slo_nodes) > 0: idp_slo_url = slo_nodes[0].get('Location', None) signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") if len(signing_nodes) > 0 or len(encryption_nodes) > 0: certs = {} if len(signing_nodes) > 0: certs['signing'] = [] for cert_node in signing_nodes: certs['signing'].append(''.join(OneLogin_Saml2_Utils.element_text(cert_node).split())) if len(encryption_nodes) > 0: certs['encryption'] = [] for cert_node in encryption_nodes: certs['encryption'].append(''.join(OneLogin_Saml2_Utils.element_text(cert_node).split())) data['idp'] = {} if idp_entity_id is not None: data['idp']['entityId'] = idp_entity_id if idp_sso_url is not None: data['idp']['singleSignOnService'] = {} data['idp']['singleSignOnService']['url'] = idp_sso_url data['idp']['singleSignOnService']['binding'] = required_sso_binding if idp_slo_url is not None: data['idp']['singleLogoutService'] = {} data['idp']['singleLogoutService']['url'] = idp_slo_url data['idp']['singleLogoutService']['binding'] = required_slo_binding if certs is not None: if (len(certs) == 1 and (('signing' in certs and len(certs['signing']) == 1) or ('encryption' in certs and len(certs['encryption']) == 1))) or \ (('signing' in certs and len(certs['signing']) == 1) and ('encryption' in certs and len(certs['encryption']) == 1 and certs['signing'][0] == certs['encryption'][0])): if 'signing' in certs: data['idp']['x509cert'] = certs['signing'][0] else: data['idp']['x509cert'] = certs['encryption'][0] else: data['idp']['x509certMulti'] = certs if want_authn_requests_signed is not None: data['security'] = {} data['security']['authnRequestsSigned'] = want_authn_requests_signed if idp_name_id_format: data['sp'] = {} data['sp']['NameIDFormat'] = idp_name_id_format return data
def generate_name_id(value, sp_nq, sp_format=None, 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 :param nq: IDP Name Qualifier :type: string :returns: DOMElement | XMLSec nameID :rtype: string """ doc = Document() name_id_container = doc.createElementNS( OneLogin_Saml2_Constants.NS_SAML, 'container') name_id_container.setAttribute("xmlns:saml", OneLogin_Saml2_Constants.NS_SAML) name_id = doc.createElement('saml:NameID') if sp_nq is not None: name_id.setAttribute('SPNameQualifier', sp_nq) if nq is not None: name_id.setAttribute('NameQualifier', nq) if sp_format is not None: name_id.setAttribute('Format', sp_format) name_id.appendChild(doc.createTextNode(value)) name_id_container.appendChild(name_id) if cert is not None: xml = name_id_container.toxml() elem = fromstring(xml, forbid_dtd=True) error_callback_method = None if debug: error_callback_method = print_xmlsec_errors xmlsec.set_error_callback(error_callback_method) # Load the public cert mngr = xmlsec.KeysMngr() file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) key_data = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) key_data.name = basename(file_cert.name) mngr.addKey(key_data) file_cert.close() # Prepare for encryption enc_data = EncData(xmlsec.TransformAes128Cbc, type=xmlsec.TypeEncElement) enc_data.ensureCipherValue() key_info = enc_data.ensureKeyInfo() # enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaPkcs1) enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaOaep) enc_key.ensureCipherValue() # Encrypt! enc_ctx = xmlsec.EncCtx(mngr) enc_ctx.encKey = xmlsec.Key.generate(xmlsec.KeyDataAes, 128, xmlsec.KeyDataTypeSession) edata = enc_ctx.encryptXml(enc_data, elem[0]) newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) if newdoc.hasChildNodes(): child = newdoc.firstChild child.removeAttribute('xmlns') child.removeAttribute('xmlns:saml') child.setAttribute('xmlns:xenc', OneLogin_Saml2_Constants.NS_XENC) child.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) nodes = newdoc.getElementsByTagName("*") for node in nodes: if node.tagName == 'ns0:KeyInfo': node.tagName = 'dsig:KeyInfo' node.removeAttribute('xmlns:ns0') node.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) else: node.tagName = 'xenc:' + node.tagName encrypted_id = newdoc.createElement('saml:EncryptedID') encrypted_data = newdoc.replaceChild(encrypted_id, newdoc.firstChild) encrypted_id.appendChild(encrypted_data) return newdoc.saveXML(encrypted_id) else: return doc.saveXML(name_id)
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 :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean """ if xml is None or xml == '': raise Exception('Empty string supplied as input') elif isinstance(xml, etree._Element): elem = xml elif isinstance(xml, Document): xml = xml.toxml() elem = fromstring(str(xml), forbid_dtd=True) elif isinstance(xml, Element): xml.setAttributeNS(unicode(OneLogin_Saml2_Constants.NS_MD), 'xmlns:md', unicode(OneLogin_Saml2_Constants.NS_MD)) xml = xml.toxml() elem = fromstring(str(xml), forbid_dtd=True) elif isinstance(xml, basestring): elem = fromstring(str(xml), forbid_dtd=True) else: raise Exception('Error parsing xml string') error_callback_method = None if debug: error_callback_method = print_xmlsec_errors xmlsec.set_error_callback(error_callback_method) xmlsec.addIDs(elem, ["ID"]) signature_nodes = OneLogin_Saml2_Utils.query( elem, '/md:EntitiesDescriptor/ds:Signature') if len(signature_nodes) == 0: signature_nodes += OneLogin_Saml2_Utils.query( elem, '/md:EntityDescriptor/ds:Signature') if len(signature_nodes) == 0: signature_nodes += OneLogin_Saml2_Utils.query( elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature') signature_nodes += OneLogin_Saml2_Utils.query( elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature') if len(signature_nodes) > 0: for signature_node in signature_nodes: OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) return True else: raise Exception( 'Could not validate metadata signature: No signature nodes found.' )
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None, multicerts=None): """ 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 :param xpath: The xpath of the signed element :type: string :param multicerts: Multiple public certs :type: list :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean """ if xml is None or xml == '': raise Exception('Empty string supplied as input') elif isinstance(xml, etree._Element): elem = xml elif isinstance(xml, Document): xml = xml.toxml() elem = fromstring(str(xml), forbid_dtd=True) elif isinstance(xml, Element): xml.setAttributeNS(unicode(OneLogin_Saml2_Constants.NS_SAMLP), 'xmlns:samlp', unicode(OneLogin_Saml2_Constants.NS_SAMLP)) xml.setAttributeNS(unicode(OneLogin_Saml2_Constants.NS_SAML), 'xmlns:saml', unicode(OneLogin_Saml2_Constants.NS_SAML)) xml = xml.toxml() elem = fromstring(str(xml), forbid_dtd=True) elif isinstance(xml, basestring): elem = fromstring(str(xml), forbid_dtd=True) else: raise Exception('Error parsing xml string') error_callback_method = None if debug: error_callback_method = print_xmlsec_errors xmlsec.set_error_callback(error_callback_method) xmlsec.addIDs(elem, ["ID"]) if xpath: signature_nodes = OneLogin_Saml2_Utils.query(elem, xpath) else: signature_nodes = OneLogin_Saml2_Utils.query( elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH) if len(signature_nodes) == 0: signature_nodes = OneLogin_Saml2_Utils.query( elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH) if len(signature_nodes) == 1: signature_node = signature_nodes[0] if not multicerts: return OneLogin_Saml2_Utils.validate_node_sign( signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) else: # If multiple certs are provided, I may ignore cert and # fingerprint provided by the method and just check the # certs multicerts fingerprint = fingerprintalg = None for cert in multicerts: if OneLogin_Saml2_Utils.validate_node_sign( signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, False, raise_exceptions=False): return True raise OneLogin_Saml2_ValidationError( 'Signature validation failed. SAML Response rejected.') else: raise OneLogin_Saml2_ValidationError( 'Expected exactly one signature node; got {}.'.format( len(signature_nodes)), OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES)