spid-es 0.0.19 → 0.0.20

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 451ae4ac0178b95e682424be4d797a503a7f54e9d7fb4594568f532117396551
4
- data.tar.gz: e68163f57cba1896b588ddd33e069719071c34a0fdaed1ebfa2d7e9f6026e179
2
+ SHA1:
3
+ metadata.gz: 2caedfbd88265d92cfa6adc54328ec47b57fd0f2
4
+ data.tar.gz: 3c2598da343c443c0b09f9ef0dfcd7cf4e0fc376
5
5
  SHA512:
6
- metadata.gz: d4a45064042c4523f8a528a7e959b9d9caf7baaaae88a492e2b3466f64a744a5e10ae93d070f1a6070e7b70975401329702463d511b2b0405dfec2b372ac4d98
7
- data.tar.gz: 63b464fc4a5a512a0aa37d2e95b0ebb29754a82bd96051ab12cb256dd6b3ebbca523edd0911f868137caaa850125eb264337372696c6b01c7aa35c0e613ef3a7
6
+ metadata.gz: aa9c67a4667aaaa532215ab7c89dacad4fed11b13cbef4e9304eca85ac6cfe0b019e0d1e273b7920565224d8730351e85c3bf7d910dd6b64a628c0cccca6abc3
7
+ data.tar.gz: 61c9e4351f6a108d1bfffd5f3978d4ec2ee3231b531e8be835bde36459892383e6ac3c588842e695a76d19a2e352ab9295f80a35fe31433a5f0ea43e1cbc4782
@@ -4,239 +4,671 @@ require "nokogiri"
4
4
  require "base64"
5
5
  require "openssl"
6
6
  require "digest/sha1"
7
+ require_relative "utils"
7
8
 
8
9
  # Only supports SAML 2.0
9
10
  module Spid
10
11
  module Saml
11
12
 
12
13
  class Response
13
- ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
14
- PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
15
- DSIG = "http://www.w3.org/2000/09/xmldsig#"
16
-
17
- attr_accessor :options, :response, :document, :settings
18
-
19
- def initialize(response, options = {})
20
- raise ArgumentError.new("Response cannot be nil") if response.nil?
21
- self.options = options
22
- self.response = response
23
- begin
24
- self.document = Spid::XMLSecurityNew::SignedDocument.new(Base64.decode64(response))
25
- rescue REXML::ParseException => e
26
- if response =~ /</
27
- self.document = Spid::XMLSecurityNew::SignedDocument.new(response)
28
- else
29
- raise e
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
30
28
  end
29
+ begin
30
+ self.document = Spid::XMLSecurityNew::SignedDocument.new(Base64.decode64(response))
31
+ rescue REXML::ParseException => e
32
+ if response =~ /</
33
+ self.document = Spid::XMLSecurityNew::SignedDocument.new(response)
34
+ else
35
+ raise e
36
+ end
37
+ end
38
+
31
39
  end
32
-
33
- end
34
40
 
35
- def is_valid?
36
- validate
37
- end
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
38
48
 
39
- def validate!
40
- validate(false)
41
- end
49
+ def is_valid?
50
+ validate
51
+ end
42
52
 
43
- # The value of the user identifier as designated by the initialization request response
44
- def name_id
45
- @name_id ||= begin
46
- node = REXML::XPath.first(document, "/saml2p:Response/saml2:Assertion[@ID='#{document.signed_element_id}']/saml2:Subject/saml2:NameID")
47
- node ||= REXML::XPath.first(document, "/saml2p:Response[@ID='#{document.signed_element_id}']/saml2:Assertion/saml2:Subject/saml2:NameID")
48
- node.nil? ? nil : node.text
53
+ def validate!
54
+ validate(false)
49
55
  end
50
- end
51
56
 
52
- # A hash of alle the attributes with the response. Assuming there is only one value for each key
53
- def attributes
54
- @attr_statements ||= begin
55
- result = {}
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
+
56
68
 
57
- stmt_element = REXML::XPath.first(document, "/p:Response/a:Assertion/a:AttributeStatement", { "p" => PROTOCOL, "a" => ASSERTION })
58
- return {} if stmt_element.nil?
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)
59
82
 
60
- stmt_element.elements.each do |attr_element|
61
- name = attr_element.attributes["Name"]
62
- value = attr_element.elements.first.text
83
+ result[name] = value
84
+ end
85
+ #mette il symbol
86
+ result.keys.each do |key|
87
+ result[key.intern] = result[key]
88
+ end
63
89
 
64
- result[name] = value
90
+ result
65
91
  end
92
+ end
66
93
 
67
- result.keys.each do |key|
68
- result[key.intern] = result[key]
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")
69
99
  end
100
+ end
101
+
70
102
 
71
- result
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
72
110
  end
73
- end
74
111
 
75
- # When this user session should expire at latest
76
- def session_expires_at
77
- @expires_at ||= begin
78
- node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:AuthnStatement", { "p" => PROTOCOL, "a" => ASSERTION })
79
- parse_time(node, "SessionNotOnOrAfter")
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?
80
116
  end
81
- end
82
-
83
- # Checks the status of the response for a "Success" code
84
- def success?
85
- @status_code ||= begin
86
- node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
87
- node.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
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
88
123
  end
89
- end
90
124
 
91
- # Conditions (if any) for the assertion to run
92
- def conditions
93
- @conditions ||= begin
94
- REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id}']/a:Conditions", { "p" => PROTOCOL, "a" => ASSERTION })
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
95
136
  end
96
- end
97
137
 
98
-
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
+ nodes = issuer_response_nodes + issuer_assertion_nodes
164
+
165
+ nodes.each{ |iss|
166
+ #controllo: L'attributo Format di Issuer deve essere presente con il valore urn:oasis:names:tc:SAML:2.0:nameid-format:entity
167
+ 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'
168
+
169
+ }
170
+
171
+
172
+ nodes.map { |node| Utils.element_text(node) }.compact.uniq
173
+ end
174
+ end
175
+
176
+
99
177
 
100
- #metodi per ricavare info per tracciatura agid
101
178
 
102
- def issuer
103
- @issuer ||= begin
104
- node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
105
- node ||= REXML::XPath.first(document, "/p:Response/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
106
- node.nil? ? nil : node.text
179
+ def response_to_id
180
+ node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
181
+ return node.attributes["InResponseTo"] unless node.blank?
107
182
  end
108
- end
109
183
 
110
- def response_to_id
111
- node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
112
- return node.attributes["InResponseTo"]
113
- end
184
+ def id
185
+ node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
186
+ return node.attributes["ID"] unless node.blank?
187
+ end
114
188
 
115
- def id
116
- node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
117
- return node.attributes["ID"]
118
- end
189
+ def issue_instant
190
+ node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
191
+ return node.attributes["IssueInstant"] unless node.blank?
192
+ end
119
193
 
120
- def issue_instant
121
- node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
122
- return node.attributes["IssueInstant"]
123
- end
194
+ def assertion_present?
195
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/", { "p" => PROTOCOL, "a" => ASSERTION })
196
+ return !node.blank?
197
+ end
124
198
 
125
- def assertion_id
126
- node = REXML::XPath.first(document, "/p:Response/a:Assertion/", { "p" => PROTOCOL, "a" => ASSERTION })
127
- return node.attributes["ID"]
128
- end
199
+ def assertion_issue_instant
200
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/", { "p" => PROTOCOL, "a" => ASSERTION })
201
+ return node.attributes["IssueInstant"] unless node.blank?
202
+ end
129
203
 
130
- def assertion_subject
131
- node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:Subject/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
132
- return node.text
133
- end
204
+ def assertion_id
205
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/", { "p" => PROTOCOL, "a" => ASSERTION })
206
+ return node.attributes["ID"] unless node.blank?
207
+ end
134
208
 
135
- def assertion_subject_name_qualifier
136
- node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:Subject/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
137
- return node.attributes["NameQualifier"]
138
- end
209
+ def assertion_subject
210
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:Subject/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
211
+ return node.text
212
+ end
139
213
 
140
-
214
+ def assertion_subject_name_qualifier
215
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:Subject/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
216
+ return node.attributes["NameQualifier"] unless node.blank?
217
+ end
141
218
 
219
+ def assertion_subject_confirmation_data_not_on_or_after
220
+ node_subj_conf_data = xpath_first_from_signed_assertion('/a:Subject/a:SubjectConfirmation/a:SubjectConfirmationData')
221
+ return node_subj_conf_data.attributes["NotOnOrAfter"] unless node_subj_conf_data.blank?
222
+ end
142
223
 
224
+ def assertion_conditions_not_before
225
+ node_cond_not_before = xpath_first_from_signed_assertion('/a:Conditions')
226
+ return node_cond_not_before.attributes["NotBefore"] unless node_cond_not_before.blank?
227
+ end
143
228
 
144
- private
229
+ def assertion_conditions_not_on_or_after
230
+ node_cond_not_on_or_after = xpath_first_from_signed_assertion('/a:Conditions')
231
+ return node_cond_not_on_or_after.attributes["NotOnOrAfter"] unless node_cond_not_on_or_after.blank?
232
+ end
145
233
 
146
- def validation_error(message)
147
- raise ValidationError.new(message)
148
- end
234
+ private
149
235
 
150
- def validate(soft = true)
151
- # prime the IdP metadata before the document validation.
152
- # The idp_cert needs to be populated before the validate_response_state method
153
-
154
- if settings
155
- Spid::Saml::Metadata.new(settings).get_idp_metadata
236
+ def validation_error(message)
237
+ raise ValidationError.new(message)
156
238
  end
157
- return false if validate_structure(soft) == false
158
- return false if validate_response_state(soft) == false
159
- return false if validate_conditions(soft) == false
160
-
161
- # Just in case a user needs to toss out the signature validation,
162
- # I'm adding in an option for it. (Sometimes canonicalization is a bitch!)
163
- return true if settings.skip_validation == true
164
-
165
- # document.validte populates the idp_cert
166
- return false if document.validate_document(get_fingerprint, soft) == false
167
-
168
- # validate response code
169
- return false if success? == false
170
239
 
171
- return true
172
- end
240
+ def validate(soft = true)
241
+ # prime the IdP metadata before the document validation.
242
+ # The idp_cert needs to be populated before the validate_response_state method
243
+
244
+ if settings
245
+ idp_metadata = Spid::Saml::Metadata.new(settings).get_idp_metadata
246
+ end
247
+
248
+ #carico nei setting l'idp_entity_id
249
+ entity_descriptor_element = REXML::XPath.first(idp_metadata,"/EntityDescriptor")
250
+ if !entity_descriptor_element.nil?
251
+ settings.idp_entity_id = entity_descriptor_element.attributes["entityID"]
252
+ end
253
+
254
+ return false if validate_structure(soft) == false
255
+ return false if validate_response_state(soft) == false
256
+ return false if validate_conditions(soft) == false
257
+ #validazione assertion firmata
258
+ return false if validate_signed_elements(soft) == false
259
+ #validazione version che sia 2.0
260
+ return false if validate_version(soft) == false
261
+ #validazione version delle asserzioni che sia 2.0
262
+ return false if validate_version_assertion(soft) == false
263
+ #validazione destination
264
+ return false if validate_destination(soft) == false
265
+ #validazione status
266
+ return false if validate_status(soft) == false
267
+ #validazione issuer
268
+ return false if validate_issuer(soft) == false
269
+ #validazioni varie su asserzioni
270
+ return false if validate_assertion(soft) == false
271
+ #validazione presenza format su attributes
272
+ return false if validate_name_format_attributes(soft) == false
273
+
274
+
275
+ # Just in case a user needs to toss out the signature validation,
276
+ # I'm adding in an option for it. (Sometimes canonicalization is a bitch!)
277
+ return true if settings.skip_validation == true
278
+
279
+ # document.validte populates the idp_cert
280
+ return false if document.validate_document(get_fingerprint, soft) == false
281
+
282
+ # validate response code
283
+ return false if success? == false
284
+
285
+ return true
286
+ end
287
+
288
+ # Validates the Issuer (Of the SAML Response and the SAML Assertion)
289
+ # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
290
+ # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
291
+ # @raise [ValidationError] if soft == false and validation fails
292
+ #
293
+ def validate_issuer(soft=true)
294
+ obtained_issuers = issuers(soft)
295
+ if obtained_issuers == false
296
+ return false #errori all'interno del metodo issuers
297
+ else
298
+ obtained_issuers.each do |iss|
173
299
 
300
+ unless Spid::Saml::Utils.uri_match?(iss, settings.idp_entity_id)
301
+ # error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
302
+ # return append_error(error_msg)
303
+ return (soft ? false : validation_error("Elemento Issuer diverso da EntityID IdP, expected: <#{settings.idp_entity_id}>, but was: <#{iss}>"))
304
+ end
305
+ end
174
306
 
175
- def validate_structure(soft = true)
176
- Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
177
- @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
178
- @xml = Nokogiri::XML(self.document.to_s)
307
+ true
308
+ end
179
309
  end
180
- if soft
181
- @schema.validate(@xml).map{ return false }
182
- else
183
- @schema.validate(@xml).map{ |error| raise(Exception.new("#{error.message}\n\n#{@xml.to_s}")) }
310
+
311
+
312
+
313
+ #validate status e status code
314
+ def validate_status(soft=true)
315
+ #controlli su status
316
+ node_status = REXML::XPath.first(document, "/p:Response/p:Status", { "p" => PROTOCOL, "a" => ASSERTION })
317
+ return (soft ? false : validation_error("Status non presente")) if node_status.blank?
318
+ #controlli su status code
319
+ node_status_code = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
320
+ return (soft ? false : validation_error("Status code non presente")) if node_status_code.blank?
321
+ return (soft ? false : validation_error("Status non presente")) unless node_status_code.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
322
+ true
323
+ end
324
+
325
+
326
+
327
+ # Validates the SAML version (2.0)
328
+ # If fails, the error is added to the errors array.
329
+ # @return [Boolean] True if the SAML Response is 2.0, otherwise returns False
330
+ #
331
+ def version(document)
332
+ @version ||= begin
333
+ node = REXML::XPath.first(
334
+ document,
335
+ "/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest",
336
+ { "p" => PROTOCOL }
337
+ )
338
+ node.nil? ? nil : node.attributes['Version']
339
+ end
184
340
  end
341
+
342
+ def version_assertion(document)
343
+ assertion_nodes = xpath_from_signed_assertion()
344
+ @version_assertion = "2.0"
345
+ #ciclo sui nodi delle asserzioni, se uno ha una versione diversa da 2.0 ritorno nil
346
+ unless assertion_nodes.blank?
347
+ assertion_nodes.each{ |ass_node|
348
+ return nil if ass_node.attributes['Version'] != "2.0"
349
+ }
350
+ end
351
+ @version_assertion
185
352
  end
186
353
 
187
- def validate_response_state(soft = true)
188
- if response.empty?
189
- return soft ? false : validation_error("Blank response")
354
+ def validate_version(soft = true)
355
+ unless version(self.document) == "2.0"
356
+ #return append_error("Unsupported SAML version")
357
+ return soft ? false : validation_error("Unsupported SAML version")
358
+ end
359
+ true
190
360
  end
191
361
 
192
- if settings.nil?
193
- return soft ? false : validation_error("No settings on response")
362
+ def validate_version_assertion(soft = true)
363
+ unless version_assertion(self.document) == "2.0"
364
+ #return append_error("Unsupported SAML version")
365
+ return soft ? false : validation_error("Unsupported SAML Assertion version")
366
+ end
367
+ true
368
+ end
369
+
370
+ def validate_signed_elements(soft = true)
371
+ signature_nodes = REXML::XPath.match(decrypted_document.nil? ? document : decrypted_document,"//ds:Signature",{"ds"=>DSIG})
372
+ signed_elements = []
373
+ verified_seis = []
374
+ verified_ids = []
375
+ signature_nodes.each do |signature_node|
376
+ signed_element = signature_node.parent.name
377
+ if signed_element != 'Response' && signed_element != 'Assertion'
378
+ return soft ? false : validation_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
379
+ #return append_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
380
+ end
381
+
382
+ if signature_node.parent.attributes['ID'].nil?
383
+ return soft ? false : validation_error("Signed Element must contain an ID. SAML Response rejected")
384
+ #return append_error("Signed Element must contain an ID. SAML Response rejected")
385
+ end
386
+
387
+ id = signature_node.parent.attributes.get_attribute("ID").value
388
+ if verified_ids.include?(id)
389
+ return soft ? false : validation_error("Duplicated ID. SAML Response rejected")
390
+ #return append_error("Duplicated ID. SAML Response rejected")
391
+ end
392
+ verified_ids.push(id)
393
+
394
+ # Check that reference URI matches the parent ID and no duplicate References or IDs
395
+ ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG})
396
+ if ref
397
+ uri = ref.attributes.get_attribute("URI")
398
+ if uri && !uri.value.empty?
399
+ sei = uri.value[1..-1]
400
+
401
+ unless sei == id
402
+ #return append_error("Found an invalid Signed Element. SAML Response rejected")
403
+ return soft ? false : validation_error("Found an invalid Signed Element. SAML Response rejected")
404
+ end
405
+
406
+ if verified_seis.include?(sei)
407
+ #return append_error("Duplicated Reference URI. SAML Response rejected")
408
+ return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
409
+ end
410
+
411
+ verified_seis.push(sei)
412
+ end
413
+ end
414
+
415
+ signed_elements << signed_element
416
+ end
417
+
418
+ unless signature_nodes.length < 3 && !signed_elements.empty?
419
+ #return append_error("Found an unexpected number of Signature Element. SAML Response rejected")
420
+ return soft ? false : validation_error("Found an unexpected number of Signature Element. SAML Response rejected")
421
+ end
422
+
423
+ #if settings.security[:want_assertions_signed] && !(signed_elements.include? "Assertion")
424
+ if !(signed_elements.include? "Assertion")
425
+ #return append_error("The Assertion of the Response is not signed and the SP requires it")
426
+ return soft ? false : validation_error("L'asserzione non è firmata.")
427
+ end
428
+
429
+ true
194
430
  end
195
431
 
196
- if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
197
- return soft ? false : validation_error("No fingerprint or certificate on settings")
432
+ def validate_structure(soft = true)
433
+ Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
434
+ @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
435
+ @xml = Nokogiri::XML(self.document.to_s)
436
+ end
437
+ if soft
438
+ @schema.validate(@xml).map{ return false }
439
+ else
440
+ @schema.validate(@xml).map{ |error| raise(Exception.new("#{error.message}\n\n#{@xml.to_s}")) }
441
+ end
198
442
  end
199
443
 
200
- true
201
- end
444
+ def validate_response_state(soft = true)
445
+ if response.empty?
446
+ return soft ? false : validation_error("Blank response")
447
+ end
202
448
 
203
- def get_fingerprint
204
- idp_metadata = Spid::Saml::Metadata.new(settings).get_idp_metadata
205
-
206
- if settings.idp_cert
207
- cert_text = Base64.decode64(settings.idp_cert)
208
- cert = OpenSSL::X509::Certificate.new(cert_text)
209
- Digest::SHA2.hexdigest(cert.to_der).upcase.scan(/../).join(":")
210
- else
211
- settings.idp_cert_fingerprint
449
+ if settings.nil?
450
+ return soft ? false : validation_error("No settings on response")
451
+ end
452
+
453
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
454
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
455
+ end
456
+
457
+ true
212
458
  end
459
+
460
+ # Validates the Destination, (If the SAML Response is received where expected).
461
+ # If the response was initialized with the :skip_destination option, this validation is skipped,
462
+ # If fails, the error is added to the errors array
463
+ # @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False
213
464
 
214
- end
465
+ # @return [String|nil] Destination attribute from the SAML Response.
466
+ #
467
+ def destination
468
+ @destination ||= begin
469
+ node = REXML::XPath.first(
470
+ document,
471
+ "/p:Response",
472
+ { "p" => PROTOCOL }
473
+ )
474
+ node.nil? ? nil : node.attributes['Destination']
475
+ end
476
+ end
215
477
 
216
- def validate_conditions(soft = true)
217
- return true if conditions.nil?
218
- return true if options[:skip_conditions]
478
+ def validate_destination(soft = true)
479
+ return (soft ? false : validation_error("La response non ha destination")) if destination.nil?
480
+ #return true if options[:skip_destination]
219
481
 
220
- if not_before = parse_time(conditions, "NotBefore")
221
- if Time.now.utc < not_before
222
- return soft ? false : validation_error("Current time is earlier than NotBefore condition")
223
- end
482
+ if destination.empty?
483
+ # error_msg = "The response has an empty Destination value"
484
+ # return append_error(error_msg)
485
+ return soft ? false : validation_error("The response has an empty Destination value")
486
+ end
487
+
488
+ return true if settings.assertion_consumer_service_url.nil? || settings.assertion_consumer_service_url.empty?
489
+
490
+ unless Spid::Saml::Utils.uri_match?(destination, settings.assertion_consumer_service_url)
491
+ # error_msg = "The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}"
492
+ # return append_error(error_msg)
493
+ return soft ? false : validation_error("The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}")
494
+ end
495
+
496
+ true
224
497
  end
225
498
 
226
- if not_on_or_after = parse_time(conditions, "NotOnOrAfter")
227
- if Time.now.utc >= not_on_or_after
228
- return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
229
- end
499
+ def validate_assertion(soft = true)
500
+ #posso avere n nodi asserzione..forse
501
+ nodes_assertion = xpath_from_signed_assertion
502
+ unless nodes_assertion.blank?
503
+ #Elemento NameID non specificato
504
+ node_name_id = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
505
+ unless node_name_id.blank?
506
+ return soft ? false : validation_error("Errore su Asserzione: NameID vuoto") if node_name_id.text.blank?
507
+ #controlli su attributo format
508
+ attr_format = node_name_id.attribute("Format")
509
+ 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
510
+ #controlli su attributo NameQualifier
511
+ attr_name_qual = node_name_id.attribute("NameQualifier")
512
+ 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
513
+ else
514
+ return soft ? false : validation_error("Errore su Asserzione: NameID non presente")
515
+
516
+ end
517
+ #Controlli su SubjectConfirmation
518
+ node_subj_conf = xpath_first_from_signed_assertion('/a:Subject/a:SubjectConfirmation')
519
+ unless node_subj_conf.blank?
520
+ #controlli su attributo Method
521
+ attr_method = node_subj_conf.attribute("Method")
522
+ 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
523
+ #Controlli su SubjectConfirmationData
524
+ node_subj_conf_data = xpath_first_from_signed_assertion('/a:Subject/a:SubjectConfirmation/a:SubjectConfirmationData')
525
+ unless node_subj_conf_data.blank?
526
+ #controllo attr Recipient, vuoto o diverso da AssertionConsumerServiceURL
527
+ attr_recipient = node_subj_conf_data.attribute("Recipient")
528
+ 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
529
+ #controllo attr InResponseTo, vuoto o diverso da ID request
530
+ node_response = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL})
531
+ id_request = node_response.attribute("InResponseTo")
532
+ attr_in_resp_to = node_subj_conf_data.attribute("InResponseTo")
533
+ 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
534
+
535
+ #controllo attr NotOnOrAfter se vuoto o non presente #63 64
536
+ attr_not_on_or_after = node_subj_conf_data.attribute("NotOnOrAfter")
537
+ return soft ? false : validation_error("Errore su Asserzione: NotOnOrAfter su SubjectConfirmationData mancante") if attr_not_on_or_after.blank?
538
+
539
+
540
+ else
541
+ return soft ? false : validation_error("Errore su Asserzione: SubjectConfirmationData non presente")
542
+ end
543
+
544
+ else
545
+ return soft ? false : validation_error("Errore su Asserzione: SubjectConfirmation non presente")
546
+ end
547
+
548
+ #Controlli su Conditions
549
+ node_conditions = xpath_first_from_signed_assertion('/a:Conditions')
550
+ unless node_conditions.blank?
551
+ attr_not_before = node_conditions.attribute("NotBefore")
552
+ return soft ? false : validation_error("Errore su Asserzione: Recipient su SubjectConfirmationData vuoto") if attr_not_before.blank? #75 76
553
+ #83 84. Assertion - Elemento AudienceRestriction di Condition mancante
554
+ node_conditions_audience_restrictions = xpath_first_from_signed_assertion('/a:Conditions/a:AudienceRestriction')
555
+ return soft ? false : validation_error("Errore su Asserzione: AudienceRestriction su Conditions vuoto") if node_conditions_audience_restrictions.blank? #83 84
556
+ #85 86 87. Assertion - Elemento Audience di AudienceRestriction mancante
557
+ node_conditions_audience_restrictions_audience = xpath_first_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience')
558
+ #Spider.logger.error "\n\n node_conditions_audience_restrictions_audience #{node_conditions_audience_restrictions_audience}"
559
+ #Spider.logger.error "\n\n settings.issuer #{settings.issuer}"
560
+ 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
561
+ else
562
+ return soft ? false : validation_error("Errore su Asserzione: Conditions non presente")
563
+ end
564
+
565
+
566
+ node_auth_stat_context_class_ref = xpath_first_from_signed_assertion('/a:AuthnStatement/a:AuthnContext/a:AuthnContextClassRef')
567
+ #Spider.logger.error "\n\n node_auth_stat_context_class_ref #{node_auth_stat_context_class_ref.text}"
568
+ return soft ? false : validation_error("Errore su Asserzione: AuthnContextClassRef di AuthnContext su AuthnStatement vuoto o non L2") if node_auth_stat_context_class_ref.blank? || ( (node_auth_stat_context_class_ref.text != 'https://www.spid.gov.it/SpidL2') && (node_auth_stat_context_class_ref.text != 'https://www.spid.gov.it/SpidL3'))
569
+
570
+ node_attr_stmt_attribute_value = xpath_first_from_signed_assertion("/a:AttributeStatement/a:Attribute/a:AttributeValue")
571
+ #Elemento AttributeStatement presente, ma sottoelemento Attribute non specificato, caso 99
572
+ return soft ? false : validation_error("Errore su Asserzione: AttributeValue di Attribute su AttributeStatement vuoto") if node_attr_stmt_attribute_value.blank?
573
+
574
+
575
+ else
576
+ return soft ? false : validation_error("Errore su Asserzione: non presente")
577
+ end
578
+ true
579
+ end
580
+
581
+
582
+ def validate_name_format_attributes(soft=true)
583
+ unless attributes.blank?
584
+ return false if @attr_name_format.blank? || (@attr_name_format.length != (attributes.length / 2))
585
+ end
586
+ true
230
587
  end
231
588
 
232
- true
233
- end
589
+ def get_fingerprint
590
+ idp_metadata = Spid::Saml::Metadata.new(settings).get_idp_metadata
591
+
592
+ if settings.idp_cert
593
+ cert_text = Base64.decode64(settings.idp_cert)
594
+ cert = OpenSSL::X509::Certificate.new(cert_text)
595
+ Digest::SHA2.hexdigest(cert.to_der).upcase.scan(/../).join(":")
596
+ else
597
+ settings.idp_cert_fingerprint
598
+ end
599
+
600
+ end
601
+
602
+ def validate_conditions(soft = true)
603
+ return true if conditions.nil?
604
+ return true if options[:skip_conditions]
234
605
 
235
- def parse_time(node, attribute)
236
- if node && node.attributes[attribute]
237
- Time.parse(node.attributes[attribute])
606
+ if not_before = parse_time(conditions, "NotBefore")
607
+ if Time.now.utc < not_before
608
+ return soft ? false : validation_error("Current time is earlier than NotBefore condition")
609
+ end
610
+ end
611
+
612
+ if not_on_or_after = parse_time(conditions, "NotOnOrAfter")
613
+ if Time.now.utc >= not_on_or_after
614
+ return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
615
+ end
616
+ end
617
+
618
+ true
238
619
  end
239
- end
240
- end
620
+
621
+ def parse_time(node, attribute)
622
+ if node && node.attributes[attribute]
623
+ Time.parse(node.attributes[attribute])
624
+ end
625
+ end
626
+
627
+ # Extracts all the appearances that matchs the subelt (pattern)
628
+ # Search on any Assertion that is signed, or has a Response parent signed
629
+ # @param subelt [String] The XPath pattern
630
+ # @return [Array of REXML::Element] Return all matches
631
+ #
632
+ def xpath_from_signed_assertion(subelt=nil)
633
+ doc = decrypted_document.nil? ? document : decrypted_document
634
+ node = REXML::XPath.match(
635
+ doc,
636
+ "/p:Response/a:Assertion[@ID=$id]#{subelt}",
637
+ { "p" => PROTOCOL, "a" => ASSERTION },
638
+ { 'id' => doc.signed_element_id }
639
+ )
640
+ node.concat( REXML::XPath.match(
641
+ doc,
642
+ "/p:Response[@ID=$id]/a:Assertion#{subelt}",
643
+ { "p" => PROTOCOL, "a" => ASSERTION },
644
+ { 'id' => doc.signed_element_id }
645
+ ))
646
+ end
647
+
648
+ # Extracts the first appearance that matchs the subelt (pattern)
649
+ # Search on any Assertion that is signed, or has a Response parent signed
650
+ # @param subelt [String] The XPath pattern
651
+ # @return [REXML::Element | nil] If any matches, return the Element
652
+ #
653
+ def xpath_first_from_signed_assertion(subelt=nil)
654
+ doc = decrypted_document.nil? ? document : decrypted_document
655
+ node = REXML::XPath.first(
656
+ doc,
657
+ "/p:Response/a:Assertion[@ID=$id]#{subelt}",
658
+ { "p" => PROTOCOL, "a" => ASSERTION },
659
+ { 'id' => doc.signed_element_id }
660
+ )
661
+ node ||= REXML::XPath.first(
662
+ doc,
663
+ "/p:Response[@ID=$id]/a:Assertion#{subelt}",
664
+ { "p" => PROTOCOL, "a" => ASSERTION },
665
+ { 'id' => doc.signed_element_id }
666
+ )
667
+ node
668
+ end
669
+
670
+
671
+ end #chiudo classe
672
+
241
673
  end
242
674
  end
@@ -5,7 +5,7 @@ module Spid
5
5
  class Settings
6
6
 
7
7
  attr_accessor :sp_name_qualifier, :sp_name_identifier, :sp_cert, :sp_external_consumer_cert, :sp_private_key, :metadata_signed, :requested_attribute,:requested_attribute_eidas_min, :requested_attribute_eidas_full, :organization
8
- attr_accessor :idp_sso_target_url, :idp_cert_fingerprint, :idp_cert, :idp_slo_target_url, :idp_metadata, :idp_metadata_ttl, :idp_name_qualifier
8
+ attr_accessor :idp_entity_id, :idp_sso_target_url, :idp_cert_fingerprint, :idp_cert, :idp_slo_target_url, :idp_metadata, :idp_metadata_ttl, :idp_name_qualifier
9
9
  attr_accessor :assertion_consumer_service_binding, :assertion_consumer_service_url, :assertion_consumer_service_index, :attribute_consuming_service_index, :hash_assertion_consumer
10
10
  attr_accessor :name_identifier_value, :name_identifier_format
11
11
  attr_accessor :sessionindex, :issuer, :destination_service_url, :authn_context, :requester_identificator
@@ -183,6 +183,43 @@ module Spid
183
183
  def self.uuid
184
184
  RUBY_VERSION < '1.9' ? "_#{@@uuid_generator.generate}" : "_#{SecureRandom.uuid}"
185
185
  end
186
+
187
+
188
+ # Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed,
189
+ # then the fully-qualified domain name and the host should performa a case-insensitive match, per the
190
+ # RFC for URIs. If Rails can not parse the string in to URL pieces, return a boolean match of the
191
+ # two strings. This maintains the previous functionality.
192
+ # @return [Boolean]
193
+ def self.uri_match?(destination_url, settings_url)
194
+ dest_uri = URI.parse(destination_url)
195
+ acs_uri = URI.parse(settings_url)
196
+
197
+ if dest_uri.scheme.nil? || acs_uri.scheme.nil? || dest_uri.host.nil? || acs_uri.host.nil?
198
+ raise URI::InvalidURIError
199
+ else
200
+ dest_uri.scheme.downcase == acs_uri.scheme.downcase &&
201
+ dest_uri.host.downcase == acs_uri.host.downcase &&
202
+ dest_uri.path == acs_uri.path &&
203
+ dest_uri.query == acs_uri.query
204
+ end
205
+ rescue URI::InvalidURIError
206
+ original_uri_match?(destination_url, settings_url)
207
+ end
208
+
209
+ # If Rails' URI.parse can't match to valid URL, default back to the original matching service.
210
+ # @return [Boolean]
211
+ def self.original_uri_match?(destination_url, settings_url)
212
+ destination_url == settings_url
213
+ end
214
+
215
+ # Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes
216
+ # that there all children other than text nodes can be ignored (e.g. comments). If nil is
217
+ # passed, nil will be returned.
218
+ def self.element_text(element)
219
+ element.texts.map(&:value).join if element
220
+ end
221
+
222
+
186
223
  end
187
224
  end
188
225
  end
data/spid-es.gemspec CHANGED
@@ -2,7 +2,7 @@ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'spid-es'
5
- s.version = '0.0.19'
5
+ s.version = '0.0.20'
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Fabiano Pavan"]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spid-es
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.19
4
+ version: 0.0.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fabiano Pavan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-07 00:00:00.000000000 Z
11
+ date: 2019-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: canonix
@@ -142,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
142
  version: '0'
143
143
  requirements: []
144
144
  rubyforge_project:
145
- rubygems_version: 2.7.8
145
+ rubygems_version: 2.2.2
146
146
  signing_key:
147
147
  specification_version: 4
148
148
  summary: SAML Ruby Tookit Spid