spid-es 0.0.19 → 0.0.20

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