@@ -17,6 +17,10 @@ class Response < SamlMessage
1717 PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
1818 DSIG = "http://www.w3.org/2000/09/xmldsig#"
1919 XENC = "http://www.w3.org/2001/04/xmlenc#"
20+ SAML_NAMESPACES = {
21+ "p" => PROTOCOL ,
22+ "a" => ASSERTION
23+ } . freeze
2024
2125 # TODO: Settings should probably be initialized too... WDYT?
2226
@@ -303,7 +307,7 @@ def issuers
303307 issuer_response_nodes = REXML ::XPath . match (
304308 document ,
305309 "/p:Response/a:Issuer" ,
306- { "p" => PROTOCOL , "a" => ASSERTION }
310+ SAML_NAMESPACES
307311 )
308312
309313 unless issuer_response_nodes . size == 1
@@ -370,7 +374,7 @@ def assertion_encrypted?
370374 ! REXML ::XPath . first (
371375 document ,
372376 "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)" ,
373- { "p" => PROTOCOL , "a" => ASSERTION }
377+ SAML_NAMESPACES
374378 ) . nil?
375379 end
376380
@@ -401,9 +405,9 @@ def validate(collect_errors = false)
401405 :validate_id ,
402406 :validate_success_status ,
403407 :validate_num_assertion ,
404- :validate_no_duplicated_attributes ,
405408 :validate_signed_elements ,
406409 :validate_structure ,
410+ :validate_no_duplicated_attributes ,
407411 :validate_in_response_to ,
408412 :validate_one_conditions ,
409413 :validate_conditions ,
@@ -444,12 +448,14 @@ def validate_success_status
444448 #
445449 def validate_structure
446450 structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
447- unless valid_saml? ( document , soft )
451+
452+ check_malformed_doc = check_malformed_doc_enabled?
453+ unless valid_saml? ( document , soft , check_malformed_doc )
448454 return append_error ( structure_error_msg )
449455 end
450456
451457 unless decrypted_document . nil?
452- unless valid_saml? ( decrypted_document , soft )
458+ unless valid_saml? ( decrypted_document , soft , check_malformed_doc )
453459 return append_error ( structure_error_msg )
454460 end
455461 end
@@ -841,31 +847,47 @@ def validate_name_id
841847 true
842848 end
843849
850+ def doc_to_validate
851+ # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response
852+ # otherwise, review if the decrypted assertion contains a signature
853+ sig_elements = REXML ::XPath . match (
854+ document ,
855+ "/p:Response[@ID=$id]/ds:Signature" ,
856+ { "p" => PROTOCOL , "ds" => DSIG } ,
857+ { 'id' => document . signed_element_id }
858+ )
859+
860+ use_original = sig_elements . size == 1 || decrypted_document . nil?
861+ doc = use_original ? document : decrypted_document
862+ if !doc . processed
863+ doc . cache_referenced_xml ( @soft , check_malformed_doc_enabled? )
864+ end
865+
866+ return doc
867+ end
868+
844869 # Validates the Signature
845870 # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
846871 # @raise [ValidationError] if soft == false and validation fails
847872 #
848873 def validate_signature
849874 error_msg = "Invalid Signature on SAML Response"
850875
851- # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response
852- # otherwise, review if the decrypted assertion contains a signature
876+ doc = doc_to_validate
877+
853878 sig_elements = REXML ::XPath . match (
854879 document ,
855880 "/p:Response[@ID=$id]/ds:Signature" ,
856881 { "p" => PROTOCOL , "ds" => DSIG } ,
857882 { 'id' => document . signed_element_id }
858883 )
859884
860- use_original = sig_elements . size == 1 || decrypted_document . nil?
861- doc = use_original ? document : decrypted_document
862-
863- # Check signature nodes
885+ # Check signature node inside assertion
864886 if sig_elements . nil? || sig_elements . size == 0
865887 sig_elements = REXML ::XPath . match (
866888 doc ,
867889 "/p:Response/a:Assertion[@ID=$id]/ds:Signature" ,
868- { "p" => PROTOCOL , "a" => ASSERTION , " ds"=> DSIG } ,
890+ SAML_NAMESPACES . merge ( { " ds"=> DSIG } ) ,
869891 { 'id' => doc . signed_element_id }
870892 )
871893 end
@@ -943,24 +965,47 @@ def name_id_node
943965 end
944966 end
945967
968+ def get_cached_signed_assertion
969+ xml = doc_to_validate . referenced_xml
970+ empty_doc = REXML ::Document . new
971+
972+ return empty_doc if xml . nil? # when no signature/reference is found, return empty document
973+
974+ root = REXML ::Document . new ( xml ) . root
975+
976+ if root . attributes [ "ID" ] != doc_to_validate . signed_element_id
977+ return empty_doc
978+ end
979+
980+ assertion = empty_doc
981+ if root . name == "Response"
982+ if REXML ::XPath . first ( root , "a:Assertion" , { "a" => ASSERTION } )
983+ assertion = REXML ::XPath . first ( root , "a:Assertion" , { "a" => ASSERTION } )
984+ elsif REXML ::XPath . first ( root , "a:EncryptedAssertion" , { "a" => ASSERTION } )
985+ assertion = decrypt_assertion ( REXML ::XPath . first ( root , "a:EncryptedAssertion" , { "a" => ASSERTION } ) )
986+ end
987+ elsif root . name == "Assertion"
988+ assertion = root
989+ end
990+
991+ assertion
992+ end
993+
994+ def signed_assertion
995+ @signed_assertion ||= get_cached_signed_assertion
996+ end
997+
946998 # Extracts the first appearance that matchs the subelt (pattern)
947999 # Search on any Assertion that is signed, or has a Response parent signed
9481000 # @param subelt [String] The XPath pattern
9491001 # @return [REXML::Element | nil] If any matches, return the Element
9501002 #
9511003 def xpath_first_from_signed_assertion ( subelt = nil )
952- doc = decrypted_document . nil? ? document : decrypted_document
1004+ doc = signed_assertion
9531005 node = REXML ::XPath . first (
9541006 doc ,
955- "/p:Response/a:Assertion[@ID=$id]#{ subelt } " ,
956- { "p" => PROTOCOL , "a" => ASSERTION } ,
957- { 'id' => doc . signed_element_id }
958- )
959- node ||= REXML ::XPath . first (
960- doc ,
961- "/p:Response[@ID=$id]/a:Assertion#{ subelt } " ,
962- { "p" => PROTOCOL , "a" => ASSERTION } ,
963- { 'id' => doc . signed_element_id }
1007+ "./#{ subelt } " ,
1008+ SAML_NAMESPACES
9641009 )
9651010 node
9661011 end
@@ -971,19 +1016,13 @@ def xpath_first_from_signed_assertion(subelt=nil)
9711016 # @return [Array of REXML::Element] Return all matches
9721017 #
9731018 def xpath_from_signed_assertion ( subelt = nil )
974- doc = decrypted_document . nil? ? document : decrypted_document
1019+ doc = signed_assertion
9751020 node = REXML ::XPath . match (
9761021 doc ,
977- "/p:Response/a:Assertion[@ID=$id]#{ subelt } " ,
978- { "p" => PROTOCOL , "a" => ASSERTION } ,
979- { 'id' => doc . signed_element_id }
1022+ "./#{ subelt } " ,
1023+ SAML_NAMESPACES
9801024 )
981- node . concat ( REXML ::XPath . match (
982- doc ,
983- "/p:Response[@ID=$id]/a:Assertion#{ subelt } " ,
984- { "p" => PROTOCOL , "a" => ASSERTION } ,
985- { 'id' => doc . signed_element_id }
986- ) )
1025+ node
9871026 end
9881027
9891028 # Generates the decrypted_document
@@ -1017,7 +1056,7 @@ def decrypt_assertion_from_document(document_copy)
10171056 encrypted_assertion_node = REXML ::XPath . first (
10181057 document_copy ,
10191058 "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)" ,
1020- { "p" => PROTOCOL , "a" => ASSERTION }
1059+ SAML_NAMESPACES
10211060 )
10221061 response_node . add ( decrypt_assertion ( encrypted_assertion_node ) )
10231062 encrypted_assertion_node . remove
@@ -1087,6 +1126,10 @@ def parse_time(node, attribute)
10871126 Time . parse ( node . attributes [ attribute ] )
10881127 end
10891128 end
1129+
1130+ def check_malformed_doc_enabled?
1131+ check_malformed_doc? ( settings )
1132+ end
10901133 end
10911134 end
10921135end
0 commit comments