ruby-saml 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/xml_security.rb CHANGED
@@ -42,6 +42,36 @@ module XMLSecurity
42
42
  NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
43
43
  Nokogiri::XML::ParseOptions::NONET
44
44
 
45
+ # Safety load the SAML Message XML
46
+ # @param document [REXML::Document] The message to be loaded
47
+ # @param check_malformed_doc [Boolean] check_malformed_doc Enable or Disable the check for malformed XML
48
+ # @return [Nokogiri::XML] The nokogiri document
49
+ # @raise [ValidationError] If there was a problem loading the SAML Message XML
50
+ def self.safe_load_xml(document, check_malformed_doc = true)
51
+ doc_str = document.to_s
52
+ if doc_str.include?("<!DOCTYPE")
53
+ raise StandardError.new("Dangerous XML detected. No Doctype nodes allowed")
54
+ end
55
+
56
+ begin
57
+ xml = Nokogiri::XML(doc_str) do |config|
58
+ config.options = self::NOKOGIRI_OPTIONS
59
+ end
60
+ rescue StandardError => error
61
+ raise StandardError.new(error.message)
62
+ end
63
+
64
+ if xml.internal_subset
65
+ raise StandardError.new("Dangerous XML detected. No Doctype nodes allowed")
66
+ end
67
+
68
+ unless xml.errors.empty?
69
+ raise StandardError.new("There were XML errors when parsing: #{xml.errors}") if check_malformed_doc
70
+ end
71
+
72
+ xml
73
+ end
74
+
45
75
  def canon_algorithm(element)
46
76
  algorithm = element
47
77
  if algorithm.is_a?(REXML::Element)
@@ -114,10 +144,8 @@ module XMLSecurity
114
144
  #<KeyInfo />
115
145
  #<Object />
116
146
  #</Signature>
117
- def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
118
- noko = Nokogiri::XML(self.to_s) do |config|
119
- config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
120
- end
147
+ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1, check_malformed_doc = true)
148
+ noko = XMLSecurity::BaseDocument.safe_load_xml(self.to_s, check_malformed_doc)
121
149
 
122
150
  signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
123
151
  signed_info_element = signature_element.add_element("ds:SignedInfo")
@@ -139,9 +167,7 @@ module XMLSecurity
139
167
  reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element))
140
168
 
141
169
  # add SignatureValue
142
- noko_sig_element = Nokogiri::XML(signature_element.to_s) do |config|
143
- config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
144
- end
170
+ noko_sig_element = XMLSecurity::BaseDocument.safe_load_xml(signature_element.to_s, check_malformed_doc)
145
171
 
146
172
  noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
147
173
  canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
@@ -190,12 +216,31 @@ module XMLSecurity
190
216
  def initialize(response, errors = [])
191
217
  super(response)
192
218
  @errors = errors
219
+ reset_elements
220
+ end
221
+
222
+ def reset_elements
223
+ @referenced_xml = nil
224
+ @cached_signed_info = nil
225
+ @signature = nil
226
+ @signature_algorithm = nil
227
+ @ref = nil
228
+ @processed = false
229
+ end
230
+
231
+ def processed
232
+ @processed
233
+ end
234
+
235
+ def referenced_xml
236
+ @referenced_xml
193
237
  end
194
238
 
195
239
  def signed_element_id
196
240
  @signed_element_id ||= extract_signed_element_id
197
241
  end
198
242
 
243
+ # Validates the referenced_xml, which is the signed part of the document
199
244
  def validate_document(idp_cert_fingerprint, soft = true, options = {})
200
245
  # get cert from response
201
246
  cert_element = REXML::XPath.first(
@@ -224,6 +269,7 @@ module XMLSecurity
224
269
  if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
225
270
  return append_error("Fingerprint mismatch", soft)
226
271
  end
272
+ base64_cert = Base64.encode64(cert.to_der)
227
273
  else
228
274
  if options[:cert]
229
275
  base64_cert = Base64.encode64(options[:cert].to_pem)
@@ -259,16 +305,22 @@ module XMLSecurity
259
305
  if idp_cert.to_pem != cert.to_pem
260
306
  return append_error("Certificate of the Signature element does not match provided certificate", soft)
261
307
  end
262
- else
263
- base64_cert = Base64.encode64(idp_cert.to_pem)
264
308
  end
265
- validate_signature(base64_cert, true)
309
+
310
+ encoded_idp_cert = Base64.encode64(idp_cert.to_pem)
311
+ validate_signature(encoded_idp_cert, true)
266
312
  end
267
313
 
268
- def validate_signature(base64_cert, soft = true)
314
+ def cache_referenced_xml(soft, check_malformed_doc = true)
315
+ reset_elements
316
+ @processed = true
269
317
 
270
- document = Nokogiri::XML(self.to_s) do |config|
271
- config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
318
+ begin
319
+ nokogiri_document = XMLSecurity::BaseDocument.safe_load_xml(self, check_malformed_doc)
320
+ rescue StandardError => error
321
+ @errors << error.message
322
+ return false if soft
323
+ raise ValidationError.new("XML load failed: #{error.message}")
272
324
  end
273
325
 
274
326
  # create a rexml document
@@ -281,13 +333,15 @@ module XMLSecurity
281
333
  {"ds"=>DSIG}
282
334
  )
283
335
 
336
+ return if sig_element.nil?
337
+
284
338
  # signature method
285
339
  sig_alg_value = REXML::XPath.first(
286
340
  sig_element,
287
341
  "./ds:SignedInfo/ds:SignatureMethod",
288
342
  {"ds"=>DSIG}
289
343
  )
290
- signature_algorithm = algorithm(sig_alg_value)
344
+ @signature_algorithm = algorithm(sig_alg_value)
291
345
 
292
346
  # get signature
293
347
  base64_signature = REXML::XPath.first(
@@ -295,7 +349,11 @@ module XMLSecurity
295
349
  "./ds:SignatureValue",
296
350
  {"ds" => DSIG}
297
351
  )
298
- signature = Base64.decode64(OneLogin::RubySaml::Utils.element_text(base64_signature))
352
+
353
+ return if base64_signature.nil?
354
+
355
+ base64_signature_text = OneLogin::RubySaml::Utils.element_text(base64_signature)
356
+ @signature = base64_signature_text.nil? ? nil : Base64.decode64(base64_signature_text)
299
357
 
300
358
  # canonicalization method
301
359
  canon_algorithm = canon_algorithm REXML::XPath.first(
@@ -304,53 +362,75 @@ module XMLSecurity
304
362
  'ds' => DSIG
305
363
  )
306
364
 
307
- noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
365
+ noko_sig_element = nokogiri_document.at_xpath('//ds:Signature', 'ds' => DSIG)
308
366
  noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
309
367
 
310
- canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
368
+ @cached_signed_info = noko_signed_info_element.canonicalize(canon_algorithm)
369
+
370
+ ### Now get the @referenced_xml to use?
371
+ rexml_signed_info = REXML::Document.new(@cached_signed_info.to_s).root
372
+
311
373
  noko_sig_element.remove
312
374
 
313
375
  # get inclusive namespaces
314
376
  inclusive_namespaces = extract_inclusive_namespaces
315
377
 
316
378
  # check digests
317
- ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
379
+ @ref = REXML::XPath.first(rexml_signed_info, "./ds:Reference", {"ds"=>DSIG})
380
+ return if @ref.nil?
381
+
382
+ reference_nodes = nokogiri_document.xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
318
383
 
319
- hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
384
+ hashed_element = reference_nodes[0]
385
+ return if hashed_element.nil?
320
386
 
321
387
  canon_algorithm = canon_algorithm REXML::XPath.first(
322
- ref,
323
- '//ds:CanonicalizationMethod',
388
+ rexml_signed_info,
389
+ './ds:CanonicalizationMethod',
324
390
  { "ds" => DSIG }
325
391
  )
326
392
 
327
- canon_algorithm = process_transforms(ref, canon_algorithm)
393
+ canon_algorithm = process_transforms(@ref, canon_algorithm)
394
+
395
+ @referenced_xml = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
396
+ end
397
+
398
+ def validate_signature(base64_cert, soft = true)
399
+ if !@processed
400
+ cache_referenced_xml(soft)
401
+ end
402
+
403
+ return append_error("No Signature Algorithm Method found", soft) if @signature_algorithm.nil?
404
+ return append_error("No Signature node found", soft) if @signature.nil?
405
+ return append_error("No canonized SignedInfo ", soft) if @cached_signed_info.nil?
406
+ return append_error("No Reference node found", soft) if @ref.nil?
407
+ return append_error("No referenced XML", soft) if @referenced_xml.nil?
328
408
 
329
- canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
409
+ # get certificate object
410
+ cert_text = Base64.decode64(base64_cert)
411
+ cert = OpenSSL::X509::Certificate.new(cert_text)
330
412
 
331
413
  digest_algorithm = algorithm(REXML::XPath.first(
332
- ref,
333
- "//ds:DigestMethod",
414
+ @ref,
415
+ "./ds:DigestMethod",
334
416
  { "ds" => DSIG }
335
417
  ))
336
- hash = digest_algorithm.digest(canon_hashed_element)
418
+ hash = digest_algorithm.digest(@referenced_xml)
337
419
  encoded_digest_value = REXML::XPath.first(
338
- ref,
339
- "//ds:DigestValue",
420
+ @ref,
421
+ "./ds:DigestValue",
340
422
  { "ds" => DSIG }
341
423
  )
342
- digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
424
+ encoded_digest_value_text = OneLogin::RubySaml::Utils.element_text(encoded_digest_value)
425
+ digest_value = encoded_digest_value_text.nil? ? nil : Base64.decode64(encoded_digest_value_text)
343
426
 
344
- unless digests_match?(hash, digest_value)
427
+ # Compare the computed "hash" with the "signed" hash
428
+ unless hash && hash == digest_value
345
429
  return append_error("Digest mismatch", soft)
346
430
  end
347
431
 
348
- # get certificate object
349
- cert_text = Base64.decode64(base64_cert)
350
- cert = OpenSSL::X509::Certificate.new(cert_text)
351
-
352
432
  # verify signature
353
- unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
433
+ unless cert.public_key.verify(@signature_algorithm.new, @signature, @cached_signed_info)
354
434
  return append_error("Key validation error", soft)
355
435
  end
356
436
 
@@ -362,7 +442,7 @@ module XMLSecurity
362
442
  def process_transforms(ref, canon_algorithm)
363
443
  transforms = REXML::XPath.match(
364
444
  ref,
365
- "//ds:Transforms/ds:Transform",
445
+ "./ds:Transforms/ds:Transform",
366
446
  { "ds" => DSIG }
367
447
  )
368
448
 
data/ruby-saml.gemspec CHANGED
@@ -59,6 +59,12 @@ Gem::Specification.new do |s|
59
59
  s.add_runtime_dependency('rexml')
60
60
  end
61
61
 
62
+ if RUBY_VERSION >= '3.4.0'
63
+ s.add_runtime_dependency("logger")
64
+ s.add_runtime_dependency("base64")
65
+ s.add_runtime_dependency('mutex_m')
66
+ end
67
+
62
68
  s.add_development_dependency('simplecov', '<0.22.0')
63
69
  if RUBY_VERSION < '2.4.1'
64
70
  s.add_development_dependency('simplecov-lcov', '<0.8.0')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-saml
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.16.0
4
+ version: 1.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SAML Toolkit
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-10-09 00:00:00.000000000 Z
12
+ date: 2025-03-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
@@ -183,6 +183,7 @@ extra_rdoc_files:
183
183
  - README.md
184
184
  files:
185
185
  - ".document"
186
+ - ".github/FUNDING.yml"
186
187
  - ".github/workflows/test.yml"
187
188
  - ".gitignore"
188
189
  - CHANGELOG.md
@@ -247,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
247
248
  - !ruby/object:Gem::Version
248
249
  version: '0'
249
250
  requirements: []
250
- rubygems_version: 3.4.1
251
+ rubygems_version: 3.5.18
251
252
  signing_key:
252
253
  specification_version: 4
253
254
  summary: SAML Ruby Tookit