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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/test.yml +49 -3
- data/CHANGELOG.md +31 -8
- data/README.md +96 -69
- data/UPGRADING.md +9 -0
- data/lib/onelogin/ruby-saml/authrequest.rb +7 -8
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +6 -8
- data/lib/onelogin/ruby-saml/logoutrequest.rb +6 -6
- data/lib/onelogin/ruby-saml/logoutresponse.rb +2 -1
- data/lib/onelogin/ruby-saml/metadata.rb +21 -25
- data/lib/onelogin/ruby-saml/response.rb +116 -52
- data/lib/onelogin/ruby-saml/saml_message.rb +21 -11
- data/lib/onelogin/ruby-saml/settings.rb +132 -31
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +7 -6
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +7 -7
- data/lib/onelogin/ruby-saml/utils.rb +86 -22
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/xml_security.rb +116 -36
- data/ruby-saml.gemspec +6 -0
- metadata +4 -3
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 =
|
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 =
|
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
|
-
|
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
|
314
|
+
def cache_referenced_xml(soft, check_malformed_doc = true)
|
315
|
+
reset_elements
|
316
|
+
@processed = true
|
269
317
|
|
270
|
-
|
271
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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(
|
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 =
|
384
|
+
hashed_element = reference_nodes[0]
|
385
|
+
return if hashed_element.nil?
|
320
386
|
|
321
387
|
canon_algorithm = canon_algorithm REXML::XPath.first(
|
322
|
-
|
323
|
-
'
|
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
|
-
|
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
|
-
"
|
414
|
+
@ref,
|
415
|
+
"./ds:DigestMethod",
|
334
416
|
{ "ds" => DSIG }
|
335
417
|
))
|
336
|
-
hash = digest_algorithm.digest(
|
418
|
+
hash = digest_algorithm.digest(@referenced_xml)
|
337
419
|
encoded_digest_value = REXML::XPath.first(
|
338
|
-
ref,
|
339
|
-
"
|
420
|
+
@ref,
|
421
|
+
"./ds:DigestValue",
|
340
422
|
{ "ds" => DSIG }
|
341
423
|
)
|
342
|
-
|
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
|
-
|
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,
|
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
|
-
"
|
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.
|
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:
|
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.
|
251
|
+
rubygems_version: 3.5.18
|
251
252
|
signing_key:
|
252
253
|
specification_version: 4
|
253
254
|
summary: SAML Ruby Tookit
|