ruby-saml 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,29 +62,14 @@ module OneLogin
62
62
  }
63
63
  end
64
64
 
65
- # Add KeyDescriptor if messages will be signed / encrypted
66
- # with SP certificate, and new SP certificate if any
65
+ # Add KeyDescriptor elements for SP certificates.
67
66
  def add_sp_certificates(sp_sso, settings)
68
- cert = settings.get_sp_cert
69
- cert_new = settings.get_sp_cert_new
70
-
71
- for sp_cert in [cert, cert_new]
72
- if sp_cert
73
- cert_text = Base64.encode64(sp_cert.to_der).gsub("\n", '')
74
- kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
75
- ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
76
- xd = ki.add_element "ds:X509Data"
77
- xc = xd.add_element "ds:X509Certificate"
78
- xc.text = cert_text
79
-
80
- if settings.security[:want_assertions_encrypted]
81
- kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" }
82
- ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
83
- xd2 = ki2.add_element "ds:X509Data"
84
- xc2 = xd2.add_element "ds:X509Certificate"
85
- xc2.text = cert_text
86
- end
87
- end
67
+ certs = settings.get_sp_certs
68
+
69
+ certs[:signing].each { |cert, _| add_sp_cert_element(sp_sso, cert, :signing) }
70
+
71
+ if settings.security[:want_assertions_encrypted]
72
+ certs[:encryption].each { |cert, _| add_sp_cert_element(sp_sso, cert, :encryption) }
88
73
  end
89
74
 
90
75
  sp_sso
@@ -153,15 +138,14 @@ module OneLogin
153
138
  def embed_signature(meta_doc, settings)
154
139
  return unless settings.security[:metadata_signed]
155
140
 
156
- private_key = settings.get_sp_key
157
- cert = settings.get_sp_cert
141
+ cert, private_key = settings.get_sp_signing_pair
158
142
  return unless private_key && cert
159
143
 
160
144
  meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
161
145
  end
162
146
 
163
147
  def output_xml(meta_doc, pretty_print)
164
- ret = ''
148
+ ret = ''.dup
165
149
 
166
150
  # pretty print the XML so IdP administrators can easily see what the SP supports
167
151
  if pretty_print
@@ -172,6 +156,18 @@ module OneLogin
172
156
 
173
157
  ret
174
158
  end
159
+
160
+ private
161
+
162
+ def add_sp_cert_element(sp_sso, cert, use)
163
+ return unless cert
164
+ cert_text = Base64.encode64(cert.to_der).gsub("\n", '')
165
+ kd = sp_sso.add_element "md:KeyDescriptor", { "use" => use.to_s }
166
+ ki = kd.add_element "ds:KeyInfo", { "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#" }
167
+ xd = ki.add_element "ds:X509Data"
168
+ xc = xd.add_element "ds:X509Certificate"
169
+ xc.text = cert_text
170
+ end
175
171
  end
176
172
  end
177
173
  end
@@ -17,6 +17,10 @@ module OneLogin
17
17
  PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
18
18
  DSIG = "http://www.w3.org/2000/09/xmldsig#"
19
19
  XENC = "http://www.w3.org/2001/04/xmlenc#"
20
+ SAML_NAMESPACES = {
21
+ "p" => PROTOCOL,
22
+ "a" => ASSERTION
23
+ }.freeze
20
24
 
21
25
  # TODO: Settings should probably be initialized too... WDYT?
22
26
 
@@ -198,6 +202,27 @@ module OneLogin
198
202
  end
199
203
  end
200
204
 
205
+ # Gets the AuthnInstant from the AuthnStatement.
206
+ # Could be used to require re-authentication if a long time has passed
207
+ # since the last user authentication.
208
+ # @return [String] AuthnInstant value
209
+ #
210
+ def authn_instant
211
+ @authn_instant ||= begin
212
+ node = xpath_first_from_signed_assertion('/a:AuthnStatement')
213
+ node.nil? ? nil : node.attributes['AuthnInstant']
214
+ end
215
+ end
216
+
217
+ # Gets the AuthnContextClassRef from the AuthnStatement
218
+ # Could be used to require re-authentication if the assertion
219
+ # did not met the requested authentication context class.
220
+ # @return [String] AuthnContextClassRef value
221
+ #
222
+ def authn_context_class_ref
223
+ @authn_context_class_ref ||= Utils.element_text(xpath_first_from_signed_assertion('/a:AuthnStatement/a:AuthnContext/a:AuthnContextClassRef'))
224
+ end
225
+
201
226
  # Checks if the Status has the "Success" code
202
227
  # @return [Boolean] True if the StatusCode is Sucess
203
228
  #
@@ -282,7 +307,7 @@ module OneLogin
282
307
  issuer_response_nodes = REXML::XPath.match(
283
308
  document,
284
309
  "/p:Response/a:Issuer",
285
- { "p" => PROTOCOL, "a" => ASSERTION }
310
+ SAML_NAMESPACES
286
311
  )
287
312
 
288
313
  unless issuer_response_nodes.size == 1
@@ -349,7 +374,7 @@ module OneLogin
349
374
  ! REXML::XPath.first(
350
375
  document,
351
376
  "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
352
- { "p" => PROTOCOL, "a" => ASSERTION }
377
+ SAML_NAMESPACES
353
378
  ).nil?
354
379
  end
355
380
 
@@ -380,9 +405,9 @@ module OneLogin
380
405
  :validate_id,
381
406
  :validate_success_status,
382
407
  :validate_num_assertion,
383
- :validate_no_duplicated_attributes,
384
408
  :validate_signed_elements,
385
409
  :validate_structure,
410
+ :validate_no_duplicated_attributes,
386
411
  :validate_in_response_to,
387
412
  :validate_one_conditions,
388
413
  :validate_conditions,
@@ -423,12 +448,14 @@ module OneLogin
423
448
  #
424
449
  def validate_structure
425
450
  structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
426
- unless valid_saml?(document, soft)
451
+
452
+ check_malformed_doc = check_malformed_doc_enabled?
453
+ unless valid_saml?(document, soft, check_malformed_doc)
427
454
  return append_error(structure_error_msg)
428
455
  end
429
456
 
430
457
  unless decrypted_document.nil?
431
- unless valid_saml?(decrypted_document, soft)
458
+ unless valid_saml?(decrypted_document, soft, check_malformed_doc)
432
459
  return append_error(structure_error_msg)
433
460
  end
434
461
  end
@@ -812,7 +839,7 @@ module OneLogin
812
839
 
813
840
  unless settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
814
841
  if name_id_spnamequalifier != settings.sp_entity_id
815
- return append_error("The SPNameQualifier value mistmatch the SP entityID value.")
842
+ return append_error("SPNameQualifier value does not match the SP entityID value.")
816
843
  end
817
844
  end
818
845
  end
@@ -820,6 +847,25 @@ module OneLogin
820
847
  true
821
848
  end
822
849
 
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
+
823
869
  # Validates the Signature
824
870
  # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
825
871
  # @raise [ValidationError] if soft == false and validation fails
@@ -827,8 +873,8 @@ module OneLogin
827
873
  def validate_signature
828
874
  error_msg = "Invalid Signature on SAML Response"
829
875
 
830
- # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response
831
- # otherwise, review if the decrypted assertion contains a signature
876
+ doc = doc_to_validate
877
+
832
878
  sig_elements = REXML::XPath.match(
833
879
  document,
834
880
  "/p:Response[@ID=$id]/ds:Signature",
@@ -836,15 +882,12 @@ module OneLogin
836
882
  { 'id' => document.signed_element_id }
837
883
  )
838
884
 
839
- use_original = sig_elements.size == 1 || decrypted_document.nil?
840
- doc = use_original ? document : decrypted_document
841
-
842
- # Check signature nodes
885
+ # Check signature node inside assertion
843
886
  if sig_elements.nil? || sig_elements.size == 0
844
887
  sig_elements = REXML::XPath.match(
845
888
  doc,
846
889
  "/p:Response/a:Assertion[@ID=$id]/ds:Signature",
847
- {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG},
890
+ SAML_NAMESPACES.merge({"ds"=>DSIG}),
848
891
  { 'id' => doc.signed_element_id }
849
892
  )
850
893
  end
@@ -915,31 +958,54 @@ module OneLogin
915
958
  begin
916
959
  encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID')
917
960
  if encrypted_node
918
- node = decrypt_nameid(encrypted_node)
961
+ decrypt_nameid(encrypted_node)
919
962
  else
920
- node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
963
+ xpath_first_from_signed_assertion('/a:Subject/a:NameID')
921
964
  end
922
965
  end
923
966
  end
924
967
 
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
+
925
998
  # Extracts the first appearance that matchs the subelt (pattern)
926
999
  # Search on any Assertion that is signed, or has a Response parent signed
927
1000
  # @param subelt [String] The XPath pattern
928
1001
  # @return [REXML::Element | nil] If any matches, return the Element
929
1002
  #
930
1003
  def xpath_first_from_signed_assertion(subelt=nil)
931
- doc = decrypted_document.nil? ? document : decrypted_document
1004
+ doc = signed_assertion
932
1005
  node = REXML::XPath.first(
933
1006
  doc,
934
- "/p:Response/a:Assertion[@ID=$id]#{subelt}",
935
- { "p" => PROTOCOL, "a" => ASSERTION },
936
- { 'id' => doc.signed_element_id }
937
- )
938
- node ||= REXML::XPath.first(
939
- doc,
940
- "/p:Response[@ID=$id]/a:Assertion#{subelt}",
941
- { "p" => PROTOCOL, "a" => ASSERTION },
942
- { 'id' => doc.signed_element_id }
1007
+ "./#{subelt}",
1008
+ SAML_NAMESPACES
943
1009
  )
944
1010
  node
945
1011
  end
@@ -950,26 +1016,20 @@ module OneLogin
950
1016
  # @return [Array of REXML::Element] Return all matches
951
1017
  #
952
1018
  def xpath_from_signed_assertion(subelt=nil)
953
- doc = decrypted_document.nil? ? document : decrypted_document
1019
+ doc = signed_assertion
954
1020
  node = REXML::XPath.match(
955
1021
  doc,
956
- "/p:Response/a:Assertion[@ID=$id]#{subelt}",
957
- { "p" => PROTOCOL, "a" => ASSERTION },
958
- { 'id' => doc.signed_element_id }
1022
+ "./#{subelt}",
1023
+ SAML_NAMESPACES
959
1024
  )
960
- node.concat( REXML::XPath.match(
961
- doc,
962
- "/p:Response[@ID=$id]/a:Assertion#{subelt}",
963
- { "p" => PROTOCOL, "a" => ASSERTION },
964
- { 'id' => doc.signed_element_id }
965
- ))
1025
+ node
966
1026
  end
967
1027
 
968
1028
  # Generates the decrypted_document
969
1029
  # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted
970
1030
  #
971
1031
  def generate_decrypted_document
972
- if settings.nil? || !settings.get_sp_key
1032
+ if settings.nil? || settings.get_sp_decryption_keys.empty?
973
1033
  raise ValidationError.new('An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method')
974
1034
  end
975
1035
 
@@ -996,7 +1056,7 @@ module OneLogin
996
1056
  encrypted_assertion_node = REXML::XPath.first(
997
1057
  document_copy,
998
1058
  "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
999
- { "p" => PROTOCOL, "a" => ASSERTION }
1059
+ SAML_NAMESPACES
1000
1060
  )
1001
1061
  response_node.add(decrypt_assertion(encrypted_assertion_node))
1002
1062
  encrypted_assertion_node.remove
@@ -1012,42 +1072,42 @@ module OneLogin
1012
1072
  end
1013
1073
 
1014
1074
  # Decrypts an EncryptedID element
1015
- # @param encryptedid_node [REXML::Element] The EncryptedID element
1075
+ # @param encrypted_id_node [REXML::Element] The EncryptedID element
1016
1076
  # @return [REXML::Document] The decrypted EncrypedtID element
1017
1077
  #
1018
- def decrypt_nameid(encryptedid_node)
1019
- decrypt_element(encryptedid_node, /(.*<\/(\w+:)?NameID>)/m)
1078
+ def decrypt_nameid(encrypted_id_node)
1079
+ decrypt_element(encrypted_id_node, /(.*<\/(\w+:)?NameID>)/m)
1020
1080
  end
1021
1081
 
1022
- # Decrypts an EncryptedID element
1023
- # @param encryptedid_node [REXML::Element] The EncryptedID element
1024
- # @return [REXML::Document] The decrypted EncrypedtID element
1082
+ # Decrypts an EncryptedAttribute element
1083
+ # @param encrypted_attribute_node [REXML::Element] The EncryptedAttribute element
1084
+ # @return [REXML::Document] The decrypted EncryptedAttribute element
1025
1085
  #
1026
- def decrypt_attribute(encryptedattribute_node)
1027
- decrypt_element(encryptedattribute_node, /(.*<\/(\w+:)?Attribute>)/m)
1086
+ def decrypt_attribute(encrypted_attribute_node)
1087
+ decrypt_element(encrypted_attribute_node, /(.*<\/(\w+:)?Attribute>)/m)
1028
1088
  end
1029
1089
 
1030
1090
  # Decrypt an element
1031
- # @param encryptedid_node [REXML::Element] The encrypted element
1032
- # @param rgrex string Regex
1091
+ # @param encrypt_node [REXML::Element] The encrypted element
1092
+ # @param regexp [Regexp] The regular expression to extract the decrypted data
1033
1093
  # @return [REXML::Document] The decrypted element
1034
1094
  #
1035
- def decrypt_element(encrypt_node, rgrex)
1036
- if settings.nil? || !settings.get_sp_key
1095
+ def decrypt_element(encrypt_node, regexp)
1096
+ if settings.nil? || settings.get_sp_decryption_keys.empty?
1037
1097
  raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it')
1038
1098
  end
1039
1099
 
1040
-
1041
1100
  if encrypt_node.name == 'EncryptedAttribute'
1042
1101
  node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
1043
1102
  else
1044
1103
  node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'
1045
1104
  end
1046
1105
 
1047
- elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key)
1106
+ elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypt_node, settings.get_sp_decryption_keys)
1107
+
1048
1108
  # If we get some problematic noise in the plaintext after decrypting.
1049
1109
  # This quick regexp parse will grab only the Element and discard the noise.
1050
- elem_plaintext = elem_plaintext.match(rgrex)[0]
1110
+ elem_plaintext = elem_plaintext.match(regexp)[0]
1051
1111
 
1052
1112
  # To avoid namespace errors if saml namespace is not defined
1053
1113
  # create a parent node first with the namespace defined
@@ -1066,6 +1126,10 @@ module OneLogin
1066
1126
  Time.parse(node.attributes[attribute])
1067
1127
  end
1068
1128
  end
1129
+
1130
+ def check_malformed_doc_enabled?
1131
+ check_malformed_doc?(settings)
1132
+ end
1069
1133
  end
1070
1134
  end
1071
1135
  end
@@ -19,15 +19,13 @@ module OneLogin
19
19
  PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol".freeze
20
20
 
21
21
  BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z)
22
- @@mutex = Mutex.new
23
22
 
24
23
  # @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema
25
24
  #
26
25
  def self.schema
27
- @@mutex.synchronize do
28
- Dir.chdir(File.expand_path("../../../schemas", __FILE__)) do
29
- ::Nokogiri::XML::Schema(File.read("saml-schema-protocol-2.0.xsd"))
30
- end
26
+ path = File.expand_path("../../../schemas/saml-schema-protocol-2.0.xsd", __FILE__)
27
+ File.open(path) do |file|
28
+ ::Nokogiri::XML::Schema(file)
31
29
  end
32
30
  end
33
31
 
@@ -60,14 +58,13 @@ module OneLogin
60
58
  # Validates the SAML Message against the specified schema.
61
59
  # @param document [REXML::Document] The message that will be validated
62
60
  # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the message is invalid or not)
61
+ # @param check_malformed_doc [Boolean] check_malformed_doc Enable or Disable the check for malformed XML
63
62
  # @return [Boolean] True if the XML is valid, otherwise False, if soft=True
64
63
  # @raise [ValidationError] if soft == false and validation fails
65
64
  #
66
- def valid_saml?(document, soft = true)
65
+ def valid_saml?(document, soft = true, check_malformed_doc = true)
67
66
  begin
68
- xml = Nokogiri::XML(document.to_s) do |config|
69
- config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
70
- end
67
+ xml = XMLSecurity::BaseDocument.safe_load_xml(document, check_malformed_doc)
71
68
  rescue StandardError => error
72
69
  return false if soft
73
70
  raise ValidationError.new("XML load failed: #{error.message}")
@@ -83,6 +80,7 @@ module OneLogin
83
80
 
84
81
  # Base64 decode and try also to inflate a SAML Message
85
82
  # @param saml [String] The deflated and encoded SAML Message
83
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
86
84
  # @return [String] The plain SAML Message
87
85
  #
88
86
  def decode_raw_saml(saml, settings = nil)
@@ -95,10 +93,16 @@ module OneLogin
95
93
 
96
94
  decoded = decode(saml)
97
95
  begin
98
- inflate(decoded)
96
+ message = inflate(decoded)
99
97
  rescue
100
- decoded
98
+ message = decoded
99
+ end
100
+
101
+ if message.bytesize > settings.message_max_bytesize
102
+ raise ValidationError.new("SAML Message exceeds " + settings.message_max_bytesize.to_s + " bytes, so was rejected")
101
103
  end
104
+
105
+ message
102
106
  end
103
107
 
104
108
  # Deflate, base64 encode and url-encode a SAML Message (To be used in the HTTP-redirect binding)
@@ -155,6 +159,12 @@ module OneLogin
155
159
  def deflate(inflated)
156
160
  Zlib::Deflate.deflate(inflated, 9)[2..-5]
157
161
  end
162
+
163
+ def check_malformed_doc?(settings)
164
+ default_value = OneLogin::RubySaml::Settings::DEFAULTS[:check_malformed_doc]
165
+
166
+ settings.nil? ? default_value : settings.check_malformed_doc
167
+ end
158
168
  end
159
169
  end
160
170
  end