ciam-es 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/Gemfile +4 -0
  4. data/README.md +127 -0
  5. data/ciam-es.gemspec +23 -0
  6. data/lib/ciam-es.rb +14 -0
  7. data/lib/ciam/ruby-saml/authrequest.rb +206 -0
  8. data/lib/ciam/ruby-saml/coding.rb +34 -0
  9. data/lib/ciam/ruby-saml/error_handling.rb +27 -0
  10. data/lib/ciam/ruby-saml/logging.rb +26 -0
  11. data/lib/ciam/ruby-saml/logout_request.rb +126 -0
  12. data/lib/ciam/ruby-saml/logout_response.rb +132 -0
  13. data/lib/ciam/ruby-saml/metadata.rb +509 -0
  14. data/lib/ciam/ruby-saml/request.rb +81 -0
  15. data/lib/ciam/ruby-saml/response.rb +683 -0
  16. data/lib/ciam/ruby-saml/settings.rb +89 -0
  17. data/lib/ciam/ruby-saml/utils.rb +225 -0
  18. data/lib/ciam/ruby-saml/validation_error.rb +7 -0
  19. data/lib/ciam/ruby-saml/version.rb +5 -0
  20. data/lib/ciam/xml_security.rb +166 -0
  21. data/lib/ciam/xml_security_new.rb +373 -0
  22. data/lib/schemas/saml20assertion_schema.xsd +283 -0
  23. data/lib/schemas/saml20protocol_schema.xsd +302 -0
  24. data/lib/schemas/xenc_schema.xsd +146 -0
  25. data/lib/schemas/xmldsig_schema.xsd +318 -0
  26. data/test/certificates/certificate1 +12 -0
  27. data/test/logoutrequest_test.rb +98 -0
  28. data/test/request_test.rb +53 -0
  29. data/test/response_test.rb +219 -0
  30. data/test/responses/adfs_response_sha1.xml +46 -0
  31. data/test/responses/adfs_response_sha256.xml +46 -0
  32. data/test/responses/adfs_response_sha384.xml +46 -0
  33. data/test/responses/adfs_response_sha512.xml +46 -0
  34. data/test/responses/no_signature_ns.xml +48 -0
  35. data/test/responses/open_saml_response.xml +56 -0
  36. data/test/responses/response1.xml.base64 +1 -0
  37. data/test/responses/response2.xml.base64 +79 -0
  38. data/test/responses/response3.xml.base64 +66 -0
  39. data/test/responses/response4.xml.base64 +93 -0
  40. data/test/responses/response5.xml.base64 +102 -0
  41. data/test/responses/response_with_ampersands.xml +139 -0
  42. data/test/responses/response_with_ampersands.xml.base64 +93 -0
  43. data/test/responses/simple_saml_php.xml +71 -0
  44. data/test/responses/wrapped_response_2.xml.base64 +150 -0
  45. data/test/settings_test.rb +43 -0
  46. data/test/test_helper.rb +65 -0
  47. data/test/xml_security_test.rb +123 -0
  48. metadata +145 -0
@@ -0,0 +1,81 @@
1
+
2
+ # A few helper functions for assembling a SAMLRequest and
3
+ # sending it to the IdP
4
+ module Ciam::Saml
5
+ include Coding
6
+ module Request
7
+
8
+ # a few symbols for SAML class names
9
+ HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
10
+ HTTP_GET = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
11
+ # get the IdP metadata, and select the appropriate SSO binding
12
+ # that we can support. Currently this is HTTP-Redirect and HTTP-POST
13
+ # but more could be added in the future
14
+ def binding_select(service)
15
+ # first check if we're still using the old hard coded method for
16
+ # backwards compatability
17
+ if @settings.idp_metadata == nil && @settings.idp_sso_target_url != nil
18
+ @URL = @settings.idp_sso_target_url
19
+ return "GET", content_get
20
+ end
21
+ # grab the metadata
22
+ metadata = Metadata::new
23
+ meta_doc = metadata.get_idp_metadata(@settings)
24
+
25
+ # first try POST
26
+ sso_element = REXML::XPath.first(meta_doc,
27
+ "/EntityDescriptor/IDPSSODescriptor/#{service}[@Binding='#{HTTP_POST}']")
28
+ if sso_element
29
+ @URL = sso_element.attributes["Location"]
30
+ #Logging.debug "binding_select: POST to #{@URL}"
31
+ return "POST", content_post
32
+ end
33
+
34
+ # next try GET
35
+ sso_element = REXML::XPath.first(meta_doc,
36
+ "/EntityDescriptor/IDPSSODescriptor/#{service}[@Binding='#{HTTP_GET}']")
37
+ if sso_element
38
+ @URL = sso_element.attributes["Location"]
39
+ Logging.debug "binding_select: GET from #{@URL}"
40
+ return "GET", content_get
41
+ end
42
+ # other types we might want to add in the future: SOAP, Artifact
43
+ end
44
+
45
+ # construct the the parameter list on the URL and return
46
+ def content_get
47
+ # compress GET requests to try and stay under that 8KB request limit
48
+ deflated_request = Zlib::Deflate.deflate(@request, 9)[2..-5]
49
+ # strict_encode64() isn't available? sub out the newlines
50
+ @request_params["SAMLRequest"] = Base64.encode64(deflated_request).gsub(/\n/, "")
51
+
52
+ Logging.debug "SAMLRequest=#{@request_params["SAMLRequest"]}"
53
+ uri = Addressable::URI.parse(@URL)
54
+ uri.query_values = @request_params
55
+ url = uri.to_s
56
+ #url = @URL + "?SAMLRequest=" + @request_params["SAMLRequest"]
57
+ Logging.debug "Sending to URL #{url}"
58
+ return url
59
+ end
60
+ # construct an HTML form (POST) and return the content
61
+ def content_post
62
+ # POST requests seem to bomb out when they're deflated
63
+ # and they probably don't need to be compressed anyway
64
+ @request_params["SAMLRequest"] = Base64.encode64(@request).gsub(/\n/, "")
65
+
66
+ #Logging.debug "SAMLRequest=#{@request_params["SAMLRequest"]}"
67
+ # kind of a cheesy method of building an HTML, form since we can't rely on Rails too much,
68
+ # and REXML doesn't work well with quote characters
69
+ str = "<html><body onLoad=\"document.getElementById('form').submit();\">\n"
70
+ str += "<form id='form' name='form' method='POST' action=\"#{@URL}\">\n"
71
+ # we could change this in the future to associate a temp auth session ID
72
+ str += "<input name='RelayState' value='ruby-saml' type='hidden' />\n"
73
+ @request_params.each_pair do |key, value|
74
+ str += "<input name=\"#{key}\" value=\"#{value}\" type='hidden' />\n"
75
+ end
76
+ str += "</form></body></html>\n"
77
+ #Logging.debug "Created form:\n#{str}"
78
+ return str
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,683 @@
1
+ require_relative "../xml_security_new"
2
+ require "time"
3
+ require "nokogiri"
4
+ require "base64"
5
+ require "openssl"
6
+ require "digest/sha1"
7
+ require_relative "utils"
8
+
9
+ # Only supports SAML 2.0
10
+ module Ciam
11
+ module Saml
12
+
13
+ class Response
14
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
15
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
16
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
17
+
18
+ attr_accessor :options, :response, :document, :settings, :attr_name_format
19
+ attr_reader :decrypted_document
20
+
21
+
22
+ def initialize(response, options = {})
23
+ raise ArgumentError.new("Response cannot be nil") if response.nil?
24
+ self.options = options
25
+ self.response = response
26
+ if assertion_encrypted?
27
+ @decrypted_document = generate_decrypted_document
28
+ end
29
+ begin
30
+ self.document = Ciam::XMLSecurityNew::SignedDocument.new(Base64.decode64(response))
31
+ rescue REXML::ParseException => e
32
+ if response =~ /</
33
+ self.document = Ciam::XMLSecurityNew::SignedDocument.new(response)
34
+ else
35
+ raise e
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ # Checks if the SAML Response contains or not an EncryptedAssertion element
42
+ # @return [Boolean] True if the SAML Response contains an EncryptedAssertion element
43
+ #
44
+ def assertion_encrypted?
45
+ false
46
+ #!REXML::XPath.first(self.document, "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", { "p" => PROTOCOL, "a" => ASSERTION }).nil?
47
+ end
48
+
49
+ def is_valid?
50
+ validate
51
+ end
52
+
53
+ def validate!
54
+ validate(false)
55
+ end
56
+
57
+ # The value of the user identifier as designated by the initialization request response
58
+ def name_id
59
+ @name_id ||= begin
60
+ node = REXML::XPath.first(document, "/saml2p:Response/saml2:Assertion[@ID='#{document.signed_element_id}']/saml2:Subject/saml2:NameID")
61
+ node ||= REXML::XPath.first(document, "/saml2p:Response[@ID='#{document.signed_element_id}']/saml2:Assertion/saml2:Subject/saml2:NameID")
62
+ node.nil? ? nil : node.text
63
+ end
64
+ end
65
+
66
+
67
+
68
+
69
+ # A hash of alle the attributes with the response. Assuming there is only one value for each key
70
+ def attributes
71
+ @attr_statements ||= begin
72
+ result = {}
73
+ stmt_element = REXML::XPath.first(document, "/p:Response/a:Assertion/a:AttributeStatement", { "p" => PROTOCOL, "a" => ASSERTION })
74
+ return {} if stmt_element.nil?
75
+
76
+ @attr_name_format = []
77
+ stmt_element.elements.each do |attr_element|
78
+ name = attr_element.attributes["Name"]
79
+ #salvo i vari format per controllare poi che non ce ne siano di null
80
+ @attr_name_format << attr_element.attributes["NameFormat"].blank? ? nil : attr_element.attributes["NameFormat"].text
81
+ value = (attr_element.elements.blank? ? nil : attr_element.elements.first.text)
82
+
83
+ result[name] = value
84
+ end
85
+ #mette il symbol
86
+ result.keys.each do |key|
87
+ result[key.intern] = result[key]
88
+ end
89
+
90
+ result
91
+ end
92
+ end
93
+
94
+ # When this user session should expire at latest
95
+ def session_expires_at
96
+ @expires_at ||= begin
97
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:AuthnStatement", { "p" => PROTOCOL, "a" => ASSERTION })
98
+ parse_time(node, "SessionNotOnOrAfter")
99
+ end
100
+ end
101
+
102
+
103
+
104
+ # Checks the status of the response for a "Success" code
105
+ def success?
106
+ @status_code ||= begin
107
+ node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
108
+ node.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success" unless node.blank?
109
+ end
110
+ end
111
+
112
+ # Ritorno il valore dello StatusMessage
113
+ def get_status_message
114
+ node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusMessage", { "p" => PROTOCOL, "a" => ASSERTION })
115
+ node.text unless node.blank?
116
+ end
117
+
118
+ # Conditions (if any) for the assertion to run
119
+ def conditions
120
+ @conditions ||= begin
121
+ REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id}']/a:Conditions", { "p" => PROTOCOL, "a" => ASSERTION })
122
+ end
123
+ end
124
+
125
+
126
+
127
+ #metodi per ricavare info per tracciatura agid
128
+
129
+
130
+ def issuer
131
+ @issuer ||= begin
132
+ node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
133
+ node ||= REXML::XPath.first(document, "/p:Response/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
134
+ node.nil? ? nil : node.text
135
+ end
136
+ end
137
+
138
+ # Gets the Issuers (from Response and Assertion).
139
+ # (returns the first node that matches the supplied xpath from the Response and from the Assertion)
140
+ # @return [Array] Array with the Issuers (REXML::Element)
141
+ #
142
+ def issuers(soft=true)
143
+ @issuers ||= begin
144
+ issuer_response_nodes = REXML::XPath.match(
145
+ document,
146
+ "/p:Response/a:Issuer",
147
+ { "p" => PROTOCOL, "a" => ASSERTION }
148
+ )
149
+
150
+ unless issuer_response_nodes.size == 1
151
+ # error_msg = "Issuer of the Response not found or multiple."
152
+ # raise ValidationError.new(error_msg)
153
+ return (soft ? false : validation_error("Issuer of the Response not found or multiple."))
154
+ end
155
+
156
+ issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
157
+ unless issuer_assertion_nodes.size == 1
158
+ # error_msg = "Issuer of the Assertion not found or multiple."
159
+ # raise ValidationError.new(error_msg)
160
+ return (soft ? false : validation_error("Issuer of the Assertion not found or multiple."))
161
+ end
162
+
163
+ issuer_response_nodes.each{ |iss|
164
+ #controllo: L'attributo Format di Issuer deve essere presente con il valore urn:oasis:names:tc:SAML:2.0:nameid-format:entity
165
+ return (soft ? false : validation_error("Elemento Issuer non ha formato corretto ")) if !iss.attributes['Format'].nil? && iss.attributes['Format'] != 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity'
166
+
167
+ }
168
+
169
+ issuer_assertion_nodes.each{ |iss|
170
+ #controllo: L'attributo Format di Issuer deve essere presente con il valore urn:oasis:names:tc:SAML:2.0:nameid-format:entity
171
+ return (soft ? false : validation_error("Elemento Issuer non ha formato corretto ")) if iss.attributes['Format'] != 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity'
172
+
173
+ }
174
+
175
+ nodes = issuer_response_nodes + issuer_assertion_nodes
176
+
177
+ nodes.map { |node| Utils.element_text(node) }.compact.uniq
178
+ end
179
+ end
180
+
181
+
182
+
183
+ def response_to_id
184
+ node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
185
+ return node.attributes["InResponseTo"] unless node.blank?
186
+ end
187
+
188
+ def id
189
+ node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
190
+ return node.attributes["ID"] unless node.blank?
191
+ end
192
+
193
+ def issue_instant
194
+ node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
195
+ return node.attributes["IssueInstant"] unless node.blank?
196
+ end
197
+
198
+ def assertion_present?
199
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/", { "p" => PROTOCOL, "a" => ASSERTION })
200
+ return !node.blank?
201
+ end
202
+
203
+ def assertion_issue_instant
204
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/", { "p" => PROTOCOL, "a" => ASSERTION })
205
+ return node.attributes["IssueInstant"] unless node.blank?
206
+ end
207
+
208
+ def assertion_id
209
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/", { "p" => PROTOCOL, "a" => ASSERTION })
210
+ return node.attributes["ID"] unless node.blank?
211
+ end
212
+
213
+ def assertion_subject
214
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:Subject/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
215
+ return node.text
216
+ end
217
+
218
+ def assertion_subject_name_qualifier
219
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:Subject/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
220
+ return node.attributes["NameQualifier"] unless node.blank?
221
+ end
222
+
223
+ def assertion_subject_confirmation_data_not_on_or_after
224
+ node_subj_conf_data = xpath_first_from_signed_assertion('/a:Subject/a:SubjectConfirmation/a:SubjectConfirmationData')
225
+ return node_subj_conf_data.attributes["NotOnOrAfter"] unless node_subj_conf_data.blank?
226
+ end
227
+
228
+ def assertion_conditions_not_before
229
+ node_cond_not_before = xpath_first_from_signed_assertion('/a:Conditions')
230
+ return node_cond_not_before.attributes["NotBefore"] unless node_cond_not_before.blank?
231
+ end
232
+
233
+ def assertion_conditions_not_on_or_after
234
+ node_cond_not_on_or_after = xpath_first_from_signed_assertion('/a:Conditions')
235
+ return node_cond_not_on_or_after.attributes["NotOnOrAfter"] unless node_cond_not_on_or_after.blank?
236
+ end
237
+
238
+ private
239
+
240
+ def validation_error(message)
241
+ raise ValidationError.new(message)
242
+ end
243
+
244
+ def validate(soft = true)
245
+ # prime the IdP metadata before the document validation.
246
+ # The idp_cert needs to be populated before the validate_response_state method
247
+
248
+ if settings
249
+ idp_metadata = Ciam::Saml::Metadata.new(settings).get_idp_metadata
250
+ end
251
+
252
+ #carico nei setting l'idp_entity_id
253
+ entity_descriptor_element = REXML::XPath.first(idp_metadata,"/EntityDescriptor")
254
+ if !entity_descriptor_element.nil?
255
+ settings.idp_entity_id = entity_descriptor_element.attributes["entityID"]
256
+ end
257
+
258
+ return false if validate_structure(soft) == false
259
+ return false if validate_response_state(soft) == false
260
+ return false if validate_conditions(soft) == false
261
+ #validazione assertion firmata
262
+ return false if validate_signed_elements(soft) == false
263
+ #validazione version che sia 2.0
264
+ return false if validate_version(soft) == false
265
+ #validazione version delle asserzioni che sia 2.0
266
+ return false if validate_version_assertion(soft) == false
267
+ #validazione destination
268
+ return false if validate_destination(soft) == false
269
+ #validazione status
270
+ return false if validate_status(soft) == false
271
+ #validazione inresponseto
272
+ return false if validate_presence_inresponseto(soft) == false
273
+ #validazione issuer
274
+ return false if validate_issuer(soft) == false
275
+ #validazioni varie su asserzioni
276
+ return false if validate_assertion(soft) == false
277
+ #validazione presenza format su attributes
278
+ return false if validate_name_format_attributes(soft) == false
279
+
280
+
281
+ # Just in case a user needs to toss out the signature validation,
282
+ # I'm adding in an option for it. (Sometimes canonicalization is a bitch!)
283
+ return true if settings.skip_validation == true
284
+
285
+ # document.validte populates the idp_cert
286
+ return false if document.validate_document(get_fingerprint, soft) == false
287
+
288
+ # validate response code
289
+ return false if success? == false
290
+
291
+ return true
292
+ end
293
+
294
+ # Validates the Issuer (Of the SAML Response and the SAML Assertion)
295
+ # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
296
+ # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
297
+ # @raise [ValidationError] if soft == false and validation fails
298
+ #
299
+ def validate_issuer(soft=true)
300
+ obtained_issuers = issuers(soft)
301
+ if obtained_issuers == false
302
+ return false #errori all'interno del metodo issuers
303
+ else
304
+ obtained_issuers.each do |iss|
305
+
306
+ unless Ciam::Saml::Utils.uri_match?(iss, settings.idp_entity_id)
307
+ # error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
308
+ # return append_error(error_msg)
309
+ return (soft ? false : validation_error("Elemento Issuer diverso da EntityID IdP, expected: <#{settings.idp_entity_id}>, but was: <#{iss}>"))
310
+ end
311
+ end
312
+
313
+ true
314
+ end
315
+ end
316
+
317
+ def validate_presence_inresponseto(soft=true)
318
+ response_to_id_value = response_to_id
319
+ return (soft ? false : validation_error("InResponseTo non specificato o mancante")) if response_to_id_value.blank?
320
+ end
321
+
322
+
323
+
324
+ #validate status e status code
325
+ def validate_status(soft=true)
326
+ #controlli su status
327
+ node_status = REXML::XPath.first(document, "/p:Response/p:Status", { "p" => PROTOCOL, "a" => ASSERTION })
328
+ return (soft ? false : validation_error("Status non presente")) if node_status.blank?
329
+ #controlli su status code
330
+ node_status_code = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
331
+ return (soft ? false : validation_error("Status code non presente")) if node_status_code.blank?
332
+ return (soft ? false : validation_error("Status non presente")) unless node_status_code.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
333
+ true
334
+ end
335
+
336
+
337
+
338
+ # Validates the SAML version (2.0)
339
+ # If fails, the error is added to the errors array.
340
+ # @return [Boolean] True if the SAML Response is 2.0, otherwise returns False
341
+ #
342
+ def version(document)
343
+ @version ||= begin
344
+ node = REXML::XPath.first(
345
+ document,
346
+ "/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest",
347
+ { "p" => PROTOCOL }
348
+ )
349
+ node.nil? ? nil : node.attributes['Version']
350
+ end
351
+ end
352
+
353
+ def version_assertion(document)
354
+ assertion_nodes = xpath_from_signed_assertion()
355
+ @version_assertion = "2.0"
356
+ #ciclo sui nodi delle asserzioni, se uno ha una versione diversa da 2.0 ritorno nil
357
+ unless assertion_nodes.blank?
358
+ assertion_nodes.each{ |ass_node|
359
+ return nil if ass_node.attributes['Version'] != "2.0"
360
+ }
361
+ end
362
+ @version_assertion
363
+ end
364
+
365
+ def validate_version(soft = true)
366
+ unless version(self.document) == "2.0"
367
+ #return append_error("Unsupported SAML version")
368
+ return soft ? false : validation_error("Unsupported SAML version")
369
+ end
370
+ true
371
+ end
372
+
373
+ def validate_version_assertion(soft = true)
374
+ unless version_assertion(self.document) == "2.0"
375
+ #return append_error("Unsupported SAML version")
376
+ return soft ? false : validation_error("Unsupported SAML Assertion version")
377
+ end
378
+ true
379
+ end
380
+
381
+ def validate_signed_elements(soft = true)
382
+ signature_nodes = REXML::XPath.match(decrypted_document.nil? ? document : decrypted_document,"//ds:Signature",{"ds"=>DSIG})
383
+ signed_elements = []
384
+ verified_seis = []
385
+ verified_ids = []
386
+ signature_nodes.each do |signature_node|
387
+ signed_element = signature_node.parent.name
388
+ if signed_element != 'Response' && signed_element != 'Assertion'
389
+ return soft ? false : validation_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
390
+ #return append_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
391
+ end
392
+
393
+ if signature_node.parent.attributes['ID'].nil?
394
+ return soft ? false : validation_error("Signed Element must contain an ID. SAML Response rejected")
395
+ #return append_error("Signed Element must contain an ID. SAML Response rejected")
396
+ end
397
+
398
+ id = signature_node.parent.attributes.get_attribute("ID").value
399
+ if verified_ids.include?(id)
400
+ return soft ? false : validation_error("Duplicated ID. SAML Response rejected")
401
+ #return append_error("Duplicated ID. SAML Response rejected")
402
+ end
403
+ verified_ids.push(id)
404
+
405
+ # Check that reference URI matches the parent ID and no duplicate References or IDs
406
+ ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG})
407
+ if ref
408
+ uri = ref.attributes.get_attribute("URI")
409
+ if uri && !uri.value.empty?
410
+ sei = uri.value[1..-1]
411
+
412
+ unless sei == id
413
+ #return append_error("Found an invalid Signed Element. SAML Response rejected")
414
+ return soft ? false : validation_error("Found an invalid Signed Element. SAML Response rejected")
415
+ end
416
+
417
+ if verified_seis.include?(sei)
418
+ #return append_error("Duplicated Reference URI. SAML Response rejected")
419
+ return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
420
+ end
421
+
422
+ verified_seis.push(sei)
423
+ end
424
+ end
425
+
426
+ signed_elements << signed_element
427
+ end
428
+
429
+ unless signature_nodes.length < 3 && !signed_elements.empty?
430
+ #return append_error("Found an unexpected number of Signature Element. SAML Response rejected")
431
+ return soft ? false : validation_error("Found an unexpected number of Signature Element. SAML Response rejected")
432
+ end
433
+
434
+ #if settings.security[:want_assertions_signed] && !(signed_elements.include? "Assertion")
435
+ if !(signed_elements.include? "Assertion")
436
+ #return append_error("The Assertion of the Response is not signed and the SP requires it")
437
+ return soft ? false : validation_error("L'asserzione non è firmata.")
438
+ end
439
+
440
+ true
441
+ end
442
+
443
+ def validate_structure(soft = true)
444
+ Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
445
+ @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
446
+ @xml = Nokogiri::XML(self.document.to_s)
447
+ end
448
+ if soft
449
+ @schema.validate(@xml).map{ return false }
450
+ else
451
+ @schema.validate(@xml).map{ |error| raise(Exception.new("#{error.message}\n\n#{@xml.to_s}")) }
452
+ end
453
+ end
454
+
455
+ def validate_response_state(soft = true)
456
+ if response.empty?
457
+ return soft ? false : validation_error("Blank response")
458
+ end
459
+
460
+ if settings.nil?
461
+ return soft ? false : validation_error("No settings on response")
462
+ end
463
+
464
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
465
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
466
+ end
467
+
468
+ true
469
+ end
470
+
471
+ # Validates the Destination, (If the SAML Response is received where expected).
472
+ # If the response was initialized with the :skip_destination option, this validation is skipped,
473
+ # If fails, the error is added to the errors array
474
+ # @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False
475
+
476
+ # @return [String|nil] Destination attribute from the SAML Response.
477
+ #
478
+ def destination
479
+ @destination ||= begin
480
+ node = REXML::XPath.first(
481
+ document,
482
+ "/p:Response",
483
+ { "p" => PROTOCOL }
484
+ )
485
+ node.nil? ? nil : node.attributes['Destination']
486
+ end
487
+ end
488
+
489
+ def validate_destination(soft = true)
490
+ return (soft ? false : validation_error("La response non ha destination")) if destination.nil?
491
+ #return true if options[:skip_destination]
492
+
493
+ if destination.empty?
494
+ # error_msg = "The response has an empty Destination value"
495
+ # return append_error(error_msg)
496
+ return soft ? false : validation_error("The response has an empty Destination value")
497
+ end
498
+
499
+ return true if settings.assertion_consumer_service_url.nil? || settings.assertion_consumer_service_url.empty?
500
+
501
+ unless Ciam::Saml::Utils.uri_match?(destination, settings.assertion_consumer_service_url)
502
+ # error_msg = "The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}"
503
+ # return append_error(error_msg)
504
+ return soft ? false : validation_error("The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}")
505
+ end
506
+
507
+ true
508
+ end
509
+
510
+ def validate_assertion(soft = true)
511
+ #posso avere n nodi asserzione..forse
512
+ nodes_assertion = xpath_from_signed_assertion
513
+ unless nodes_assertion.blank?
514
+ #Elemento NameID non specificato
515
+ node_name_id = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
516
+ unless node_name_id.blank?
517
+ return soft ? false : validation_error("Errore su Asserzione: NameID vuoto") if node_name_id.text.blank?
518
+ #controlli su attributo format
519
+ attr_format = node_name_id.attribute("Format")
520
+ return soft ? false : validation_error("Errore su Asserzione: Format su NameID vuoto") if attr_format.blank? || attr_format.to_s != "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" #45
521
+ #controlli su attributo NameQualifier
522
+ attr_name_qual = node_name_id.attribute("NameQualifier")
523
+ return soft ? false : validation_error("Errore su Asserzione: NameQualifier su NameID vuoto") if attr_name_qual.blank? || ( !attr_name_qual.blank? && attr_name_qual.value.blank?)#48 e 49
524
+ else
525
+ return soft ? false : validation_error("Errore su Asserzione: NameID non presente")
526
+
527
+ end
528
+ #Controlli su SubjectConfirmation
529
+ node_subj_conf = xpath_first_from_signed_assertion('/a:Subject/a:SubjectConfirmation')
530
+ unless node_subj_conf.blank?
531
+ #controlli su attributo Method
532
+ attr_method = node_subj_conf.attribute("Method")
533
+ return soft ? false : validation_error("Errore su Asserzione: Method su SubjectConfirmation vuoto") if attr_method.blank? || attr_method.to_s != "urn:oasis:names:tc:SAML:2.0:cm:bearer" #53 54 e 55
534
+ #Controlli su SubjectConfirmationData
535
+ node_subj_conf_data = xpath_first_from_signed_assertion('/a:Subject/a:SubjectConfirmation/a:SubjectConfirmationData')
536
+ unless node_subj_conf_data.blank?
537
+ #controllo attr Recipient, vuoto o diverso da AssertionConsumerServiceURL
538
+ attr_recipient = node_subj_conf_data.attribute("Recipient")
539
+ return soft ? false : validation_error("Errore su Asserzione: Recipient su SubjectConfirmationData vuoto o diverso da AssertionConsumerServiceURL") if attr_recipient.blank? || attr_recipient.to_s != settings.assertion_consumer_service_url #57 58 e 59
540
+ #controllo attr InResponseTo, vuoto o diverso da ID request
541
+ node_response = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL})
542
+ id_request = node_response.attribute("InResponseTo")
543
+ attr_in_resp_to = node_subj_conf_data.attribute("InResponseTo")
544
+ return soft ? false : validation_error("Errore su Asserzione: InResponseTo su SubjectConfirmationData vuoto o diverso da ID request") if attr_in_resp_to.blank? || attr_in_resp_to.to_s != id_request.to_s #57 58 e 59
545
+
546
+ #controllo attr NotOnOrAfter se vuoto o non presente #63 64
547
+ attr_not_on_or_after = node_subj_conf_data.attribute("NotOnOrAfter")
548
+ return soft ? false : validation_error("Errore su Asserzione: NotOnOrAfter su SubjectConfirmationData mancante") if attr_not_on_or_after.blank?
549
+
550
+
551
+ else
552
+ return soft ? false : validation_error("Errore su Asserzione: SubjectConfirmationData non presente")
553
+ end
554
+
555
+ else
556
+ return soft ? false : validation_error("Errore su Asserzione: SubjectConfirmation non presente")
557
+ end
558
+
559
+ #Controlli su Conditions
560
+ node_conditions = xpath_first_from_signed_assertion('/a:Conditions')
561
+ unless node_conditions.blank?
562
+ attr_not_before = node_conditions.attribute("NotBefore")
563
+ return soft ? false : validation_error("Errore su Asserzione: Recipient su SubjectConfirmationData vuoto") if attr_not_before.blank? #75 76
564
+ #83 84. Assertion - Elemento AudienceRestriction di Condition mancante
565
+ node_conditions_audience_restrictions = xpath_first_from_signed_assertion('/a:Conditions/a:AudienceRestriction')
566
+ return soft ? false : validation_error("Errore su Asserzione: AudienceRestriction su Conditions vuoto") if node_conditions_audience_restrictions.blank? #83 84
567
+ #85 86 87. Assertion - Elemento Audience di AudienceRestriction mancante
568
+ node_conditions_audience_restrictions_audience = xpath_first_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience')
569
+ #Ciamer.logger.error "\n\n node_conditions_audience_restrictions_audience #{node_conditions_audience_restrictions_audience}"
570
+ #Ciamer.logger.error "\n\n settings.issuer #{settings.issuer}"
571
+ return soft ? false : validation_error("Errore su Asserzione: Audience su AudienceRestriction vuoto") if node_conditions_audience_restrictions_audience.blank? || node_conditions_audience_restrictions_audience.text != settings.issuer #83 84
572
+ else
573
+ return soft ? false : validation_error("Errore su Asserzione: Conditions non presente")
574
+ end
575
+
576
+
577
+ node_auth_stat_context_class_ref = xpath_first_from_signed_assertion('/a:AuthnStatement/a:AuthnContext/a:AuthnContextClassRef')
578
+
579
+ node_attr_stmt_attribute_value = xpath_first_from_signed_assertion("/a:AttributeStatement/a:Attribute/a:AttributeValue")
580
+ #Elemento AttributeStatement presente, ma sottoelemento Attribute non specificato, caso 99
581
+ return soft ? false : validation_error("Errore su Asserzione: AttributeValue di Attribute su AttributeStatement vuoto") if node_attr_stmt_attribute_value.blank?
582
+
583
+
584
+ else
585
+ return soft ? false : validation_error("Errore su Asserzione: non presente")
586
+ end
587
+ true
588
+ end
589
+
590
+
591
+ def validate_name_format_attributes(soft=true)
592
+ unless attributes.blank?
593
+ return false if @attr_name_format.blank? || (@attr_name_format.length != (attributes.length / 2))
594
+ end
595
+ true
596
+ end
597
+
598
+ def get_fingerprint
599
+ idp_metadata = Ciam::Saml::Metadata.new(settings).get_idp_metadata
600
+
601
+ if settings.idp_cert
602
+ cert_text = Base64.decode64(settings.idp_cert)
603
+ cert = OpenSSL::X509::Certificate.new(cert_text)
604
+ Digest::SHA2.hexdigest(cert.to_der).upcase.scan(/../).join(":")
605
+ else
606
+ settings.idp_cert_fingerprint
607
+ end
608
+
609
+ end
610
+
611
+ def validate_conditions(soft = true)
612
+ return true if conditions.nil?
613
+ return true if options[:skip_conditions]
614
+
615
+ if not_before = parse_time(conditions, "NotBefore")
616
+ if Time.now.utc < not_before
617
+ return soft ? false : validation_error("Current time is earlier than NotBefore condition")
618
+ end
619
+ end
620
+
621
+ if not_on_or_after = parse_time(conditions, "NotOnOrAfter")
622
+ if Time.now.utc >= not_on_or_after
623
+ return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
624
+ end
625
+ end
626
+
627
+ true
628
+ end
629
+
630
+ def parse_time(node, attribute)
631
+ if node && node.attributes[attribute]
632
+ Time.parse(node.attributes[attribute])
633
+ end
634
+ end
635
+
636
+ # Extracts all the appearances that matchs the subelt (pattern)
637
+ # Search on any Assertion that is signed, or has a Response parent signed
638
+ # @param subelt [String] The XPath pattern
639
+ # @return [Array of REXML::Element] Return all matches
640
+ #
641
+ def xpath_from_signed_assertion(subelt=nil)
642
+ doc = decrypted_document.nil? ? document : decrypted_document
643
+ node = REXML::XPath.match(
644
+ doc,
645
+ "/p:Response/a:Assertion[@ID=$id]#{subelt}",
646
+ { "p" => PROTOCOL, "a" => ASSERTION },
647
+ { 'id' => doc.signed_element_id }
648
+ )
649
+ node.concat( REXML::XPath.match(
650
+ doc,
651
+ "/p:Response[@ID=$id]/a:Assertion#{subelt}",
652
+ { "p" => PROTOCOL, "a" => ASSERTION },
653
+ { 'id' => doc.signed_element_id }
654
+ ))
655
+ end
656
+
657
+ # Extracts the first appearance that matchs the subelt (pattern)
658
+ # Search on any Assertion that is signed, or has a Response parent signed
659
+ # @param subelt [String] The XPath pattern
660
+ # @return [REXML::Element | nil] If any matches, return the Element
661
+ #
662
+ def xpath_first_from_signed_assertion(subelt=nil)
663
+ doc = decrypted_document.nil? ? document : decrypted_document
664
+ node = REXML::XPath.first(
665
+ doc,
666
+ "/p:Response/a:Assertion[@ID=$id]#{subelt}",
667
+ { "p" => PROTOCOL, "a" => ASSERTION },
668
+ { 'id' => doc.signed_element_id }
669
+ )
670
+ node ||= REXML::XPath.first(
671
+ doc,
672
+ "/p:Response[@ID=$id]/a:Assertion#{subelt}",
673
+ { "p" => PROTOCOL, "a" => ASSERTION },
674
+ { 'id' => doc.signed_element_id }
675
+ )
676
+ node
677
+ end
678
+
679
+
680
+ end #chiudo classe
681
+
682
+ end
683
+ end