ciam-es 0.0.1

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.
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