ruby-saml 1.17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 188af740c1b9627be73d3a4cbd8261316773d3b3da0b74b0ef49b5d1c9c04f02
4
- data.tar.gz: 85cb561ba597924b7037be197dac6fd175c1a626969829e170bb44d8e8a1a50d
3
+ metadata.gz: 59cce47afe1159fc5674f2eaf8f0392d12df280cc97fa2a4ccd7926daf989445
4
+ data.tar.gz: 30a49c237e7a88328745b788d62e9ea92e87342fc571d20e002eb4feddd964ae
5
5
  SHA512:
6
- metadata.gz: 0b154ce0a94f1f1b525179d33f8a98d5422cabafe527f32104646135ae0a9218064638639b7ec5735de1dda8ef55faa5571f22f563d57f44e040a81b6116b5a1
7
- data.tar.gz: 78ea0021299530423e28935782b851894ed9740c1e93c610575518532d22bcc20829dbe26b0569f38ad21894cc145b3d785c75765c8bd0dde6e078598d864545
6
+ metadata.gz: c67ec4923ca4bc5e07736717d1e40f604296c95c00d1fe8ec7d28c586988b368d566a16782c676c2ce27d38d6d7e1386a5347bfbb40efc98eb033525605f5dcb
7
+ data.tar.gz: 4944ad0dc2a3999e78da570aa8faa6282e26868606a5011d2b623a6ef0a9150d2e6ee08a4245d090e5d4837a9dc9ffaa78bf46d84fe466ea9384662d305d4c94
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Ruby SAML Changelog
2
2
 
3
+ ### 1.18.0 (Mar 12, 2025)
4
+ * [#750](https://github.com/SAML-Toolkits/ruby-saml/pull/750) Fix vulnerabilities: CVE-2025-25291, CVE-2025-25292: SAML authentication bypass via Signature Wrapping attack allowed due parser differential. Fix vulnerability: CVE-2025-25293: Potential DOS abusing of compressed messages.
5
+ * [#718](https://github.com/SAML-Toolkits/ruby-saml/pull/718/) Add support to retrieve from SAMLResponse the AuthnInstant and AuthnContextClassRef values
6
+ * [#720](https://github.com/SAML-Toolkits/ruby-saml/pull/720) Fix ambiguous regex warnings
7
+ * [#715](https://github.com/SAML-Toolkits/ruby-saml/pull/715) Fix typo in SPNameQualifier error text
8
+
3
9
  ### 1.17.0 (Sep 10, 2024)
4
10
  * Fix for critical vulnerability CVE-2024-45409: SAML authentication bypass via Incorrect XPath selector
5
11
  * [#687](https://github.com/SAML-Toolkits/ruby-saml/pull/687) Add CI coverage for Ruby 3.3 and Windows.
@@ -15,7 +21,7 @@
15
21
 
16
22
  ### 1.15.0 (Jan 04, 2023)
17
23
  * [#650](https://github.com/SAML-Toolkits/ruby-saml/pull/650) Replace strip! by strip on compute_digest method
18
- * [#638](https://github.com/SAML-Toolkits/ruby-saml/pull/638) Fix dateTime format for the validUntil attribute of the generated metadata
24
+ * [#638](https://github.com/SAML-Toolkits/ruby-saml/pull/638) Fix dateTime format for the validUntil attribute of the generated metadata
19
25
  * [#576](https://github.com/SAML-Toolkits/ruby-saml/pull/576) Support `Settings#idp_cert_multi` with string keys
20
26
  * [#567](https://github.com/SAML-Toolkits/ruby-saml/pull/567) Improve Code quality
21
27
  * Add info about new repo, new maintainer, new security contact
@@ -40,6 +46,9 @@
40
46
  * Add warning about the use of IdpMetadataParser class and SSRF
41
47
  * CI: Migrate from Travis to Github Actions
42
48
 
49
+ ### 1.12.4 (Mar 12, 2025)
50
+ * [#750](https://github.com/SAML-Toolkits/ruby-saml/pull/750) Fix vulnerabilities: CVE-2025-25291, CVE-2025-25292: SAML authentication bypass via Signature Wrapping attack allowed due parser differential. Fix vulnerability: CVE-2025-25293: Potential DOS abusing of compressed messages.
51
+
43
52
  ### 1.12.3 (Sep 10, 2024)
44
53
  * Fix for critical vulnerability CVE-2024-45409: SAML authentication bypass via Incorrect XPath selector
45
54
 
@@ -52,7 +61,7 @@
52
61
 
53
62
  ### 1.12.0 (Feb 18, 2021)
54
63
  * Support AES-128-GCM, AES-192-GCM, and AES-256-GCM encryptions
55
- * Parse & return SLO ResponseLocation in IDPMetadataParser & Settings
64
+ * Parse & return SLO ResponseLocation in IDPMetadataParser & Settings
56
65
  * Adding idp_sso_service_url and idp_slo_service_url settings
57
66
  * [#536](https://github.com/onelogin/ruby-saml/pull/536) Adding feth method to be able retrieve attributes based on regex
58
67
  * Reduce size of built gem by excluding the test folder
@@ -184,7 +193,7 @@
184
193
  * Fix response_test.rb of gem 1.3.0
185
194
  * Add reference to Security Guidelines
186
195
  * Update License
187
- * [#334](https://github.com/onelogin/ruby-saml/pull/334) Keep API backward-compatibility on IdpMetadataParser fingerprint method.
196
+ * [#334](https://github.com/onelogin/ruby-saml/pull/334) Keep API backward-compatibility on IdpMetadataParser fingerprint method.
188
197
 
189
198
  ### 1.3.0 (June 24, 2016)
190
199
  * [Security Fix](https://github.com/onelogin/ruby-saml/commit/a571f52171e6bfd87db59822d1d9e8c38fb3b995) Add extra validations to prevent Signature wrapping attacks
@@ -202,7 +211,7 @@
202
211
  * [#316](https://github.com/onelogin/ruby-saml/pull/316) Fix Misspelling of transation_id to transaction_id
203
212
  * [#321](https://github.com/onelogin/ruby-saml/pull/321) Support Attribute Names on IDPSSODescriptor parser
204
213
  * Changes on empty URI of Signature reference management
205
- * [#320](https://github.com/onelogin/ruby-saml/pull/320) Dont mutate document to fix lack of reference URI
214
+ * [#320](https://github.com/onelogin/ruby-saml/pull/320) Dont mutate document to fix lack of reference URI
206
215
  * [#306](https://github.com/onelogin/ruby-saml/pull/306) Support WantAssertionsSigned
207
216
 
208
217
  ### 1.1.2 (February 15, 2016)
@@ -219,9 +228,9 @@
219
228
  * [#270](https://github.com/onelogin/ruby-saml/pull/270) Allow SAML elements to come from any namespace (at decryption process)
220
229
  * [#261](https://github.com/onelogin/ruby-saml/pull/261) Allow validate_subject_confirmation Response validation to be skipped
221
230
  * [#258](https://github.com/onelogin/ruby-saml/pull/258) Fix allowed_clock_drift on the validate_session_expiration test
222
- * [#256](https://github.com/onelogin/ruby-saml/pull/256) Separate the create_authentication_xml_doc in two methods.
231
+ * [#256](https://github.com/onelogin/ruby-saml/pull/256) Separate the create_authentication_xml_doc in two methods.
223
232
  * [#255](https://github.com/onelogin/ruby-saml/pull/255) Refactor validate signature.
224
- * [#254](https://github.com/onelogin/ruby-saml/pull/254) Handle empty URI references
233
+ * [#254](https://github.com/onelogin/ruby-saml/pull/254) Handle empty URI references
225
234
  * [#251](https://github.com/onelogin/ruby-saml/pull/251) Support qualified and unqualified NameID in attributes
226
235
  * [#234](https://github.com/onelogin/ruby-saml/pull/234) Add explicit support for JRuby
227
236
 
@@ -229,7 +238,7 @@
229
238
  * [#247](https://github.com/onelogin/ruby-saml/pull/247) Avoid entity expansion (XEE attacks)
230
239
  * [#246](https://github.com/onelogin/ruby-saml/pull/246) Fix bug generating Logout Response (issuer was at wrong order)
231
240
  * [#243](https://github.com/onelogin/ruby-saml/issues/243) and [#244](https://github.com/onelogin/ruby-saml/issues/244) Fix metadata builder errors. Fix metadata xsd.
232
- * [#241](https://github.com/onelogin/ruby-saml/pull/241) Add decrypt support (EncryptID and EncryptedAssertion). Improve compatibility with namespaces.
241
+ * [#241](https://github.com/onelogin/ruby-saml/pull/241) Add decrypt support (EncryptID and EncryptedAssertion). Improve compatibility with namespaces.
233
242
  * [#240](https://github.com/onelogin/ruby-saml/pull/240) and [#238](https://github.com/onelogin/ruby-saml/pull/238) Improve test coverage and refactor.
234
243
  * [#239](https://github.com/onelogin/ruby-saml/pull/239) Improve security: Add more validations to SAMLResponse, LogoutRequest and LogoutResponse. Refactor code and improve tests coverage.
235
244
  * [#237](https://github.com/onelogin/ruby-saml/pull/237) Don't pretty print metadata by default.
data/README.md CHANGED
@@ -4,10 +4,37 @@
4
4
  [![Rubygem Version](https://badge.fury.io/rb/ruby-saml.svg)](https://badge.fury.io/rb/ruby-saml)
5
5
  [![GitHub version](https://badge.fury.io/gh/SAML-Toolkits%2Fruby-saml.svg)](https://badge.fury.io/gh/SAML-Toolkits%2Fruby-saml) ![GitHub](https://img.shields.io/github/license/SAML-Toolkits/ruby-saml) ![Gem](https://img.shields.io/gem/dtv/ruby-saml?label=gem%20downloads%20latest) ![Gem](https://img.shields.io/gem/dt/ruby-saml?label=gem%20total%20downloads)
6
6
 
7
- Ruby SAML minor and tiny versions may introduce breaking changes. Please read
7
+ Minor and patch versions of Ruby SAML may introduce breaking changes. Please read
8
8
  [UPGRADING.md](UPGRADING.md) for guidance on upgrading to new Ruby SAML versions.
9
9
 
10
- There is a critical vulnerability affecting ruby-saml < 1.17.0 (CVE-2024-45409). Make sure you are using an updated version. (1.12.3 is safe)
10
+ ### Pay it Forward: Support RubySAML and Strengthen Open-Source Security
11
+
12
+ RubySAML is a trusted authentication library used by startups and enterprises alike.
13
+
14
+ But security doesn't happen in a vacuum. Vulnerabilities in authentication libraries can
15
+ have widespread consequences. Maintaining open-source security requires continuous
16
+ effort, expertise, and funding. By supporting RubySAML, you’re not just securing your
17
+ own systems—you’re strengthening auth security globally.
18
+
19
+ #### How you can help
20
+
21
+ * Sponsor RubySAML: [GitHub Sponsors](https://github.com/sponsors/SAML-Toolkits)
22
+ * Contribute to secure-by-design improvements
23
+ * Responsibly report vulnerabilities (see "Vulnerability Reporting" above)
24
+
25
+ Security is a shared responsibility. If RubySAML has helped your organization, please
26
+ consider giving back. Together, we can keep authentication secure.
27
+
28
+ ### Sponsors
29
+
30
+ Thanks to the following sponsors for securing the open source ecosystem,
31
+
32
+ [<img alt="84codes" src="https://avatars.githubusercontent.com/u/5353257" width="75px">](https://www.84codes.com)
33
+
34
+
35
+ ## Vulnerabilities
36
+
37
+ There are critical vulnerabilities affecting ruby-saml < 1.18.0, two of them allows SAML authentication bypass (CVE-2025-25291, CVE-2025-25292, CVE-2025-25293). Please upgrade to a fixed version (1.18.0)
11
38
 
12
39
  ## Overview
13
40
 
@@ -15,7 +42,7 @@ The Ruby SAML library is for implementing the client side of a SAML authorizatio
15
42
  i.e. it provides a means for managing authorization initialization and confirmation
16
43
  requests from identity providers.
17
44
 
18
- SAML authorization is a two step process and you are expected to implement support for both.
45
+ SAML authorization is a two-step process and you are expected to implement support for both.
19
46
 
20
47
  We created a demo project for Rails 4 that uses the latest version of this library:
21
48
  [ruby-saml-example](https://github.com/saml-toolkits/ruby-saml-example)
@@ -34,7 +61,7 @@ The following Ruby versions are covered by CI testing:
34
61
  * Make your feature addition or bug fix
35
62
  * Add tests for your new features. This is important so we don't break any features in a future version unintentionally.
36
63
  * Ensure all tests pass by running `bundle exec rake test`.
37
- * Do not change rakefile, version, or history.
64
+ * Do not change Rakefile, version, or history.
38
65
  * Open a pull request, following [this template](https://gist.github.com/Lordnibbler/11002759).
39
66
 
40
67
  ## Security Guidelines
@@ -45,21 +72,20 @@ by mail to the maintainer: sixto.martin.garcia+security@gmail.com
45
72
  ### Security Warning
46
73
 
47
74
  Some tools may incorrectly report ruby-saml is a potential security vulnerability.
48
- ruby-saml depends on Nokogiri, and it's possible to use Nokogiri in a dangerous way
75
+ ruby-saml depends on Nokogiri, and it is possible to use Nokogiri in a dangerous way
49
76
  (by enabling its DTDLOAD option and disabling its NONET option).
50
77
  This dangerous Nokogiri configuration, which is sometimes used by other components,
51
78
  can create an XML External Entity (XXE) vulnerability if the XML data is not trusted.
52
79
  However, ruby-saml never enables this dangerous Nokogiri configuration;
53
80
  ruby-saml never enables DTDLOAD, and it never disables NONET.
54
81
 
55
- The OneLogin::RubySaml::IdpMetadataParser class does not validate in any way the URL
56
- that is introduced in order to be parsed.
82
+ The OneLogin::RubySaml::IdpMetadataParser class does not validate the provided URL before parsing.
57
83
 
58
- Usually the same administrator that handles the Service Provider also sets the URL to
84
+ Usually, the same administrator who handles the Service Provider also sets the URL to
59
85
  the IdP, which should be a trusted resource.
60
86
 
61
- But there are other scenarios, like a SAAS app where the administrator of the app
62
- delegates this functionality to other users. In this case, extra precaution should
87
+ But there are other scenarios, like a SaaS app where the administrator of the app
88
+ delegates this functionality to other users. In this case, extra precautions should
63
89
  be taken in order to validate such URL inputs and avoid attacks like SSRF.
64
90
 
65
91
  ## Getting Started
@@ -71,7 +97,7 @@ Using `Gemfile`
71
97
 
72
98
  ```ruby
73
99
  # latest stable
74
- gem 'ruby-saml', '~> 1.11.0'
100
+ gem 'ruby-saml', '~> 1.17.0'
75
101
 
76
102
  # or track master for bleeding-edge
77
103
  gem 'ruby-saml', :github => 'saml-toolkit/ruby-saml'
@@ -116,8 +142,8 @@ gem install nokogiri --version '~> 1.5.10'
116
142
  ### Configuring Logging
117
143
 
118
144
  When troubleshooting SAML integration issues, you will find it extremely helpful to examine the
119
- output of this gem's business logic. By default, log messages are emitted to RAILS_DEFAULT_LOGGER
120
- when the gem is used in a Rails context, and to STDOUT when the gem is used outside of Rails.
145
+ output of this gem's business logic. By default, log messages are emitted to `RAILS_DEFAULT_LOGGER`
146
+ when the gem is used in a Rails context, and to `STDOUT` when the gem is used outside of Rails.
121
147
 
122
148
  To override the default behavior and control the destination of log messages, provide
123
149
  a ruby Logger object to the gem's logging singleton:
@@ -140,7 +166,7 @@ def init
140
166
  end
141
167
  ```
142
168
 
143
- If the SP knows who should be authenticated in the IdP, then can provide that info as follows:
169
+ If the SP knows who should be authenticated in the IdP, it can provide that info as follows:
144
170
 
145
171
  ```ruby
146
172
  def init
@@ -218,7 +244,7 @@ def saml_settings
218
244
  end
219
245
  ```
220
246
 
221
- The use of settings.issuer is deprecated in favour of settings.sp_entity_id since version 1.11.0
247
+ The use of `settings.issuer` is deprecated in favor of `settings.sp_entity_id` since version 1.11.0
222
248
 
223
249
  Some assertion validations can be skipped by passing parameters to `OneLogin::RubySaml::Response.new()`.
224
250
  For example, you can skip the `AuthnStatement`, `Conditions`, `Recipient`, or the `SubjectConfirmation`
@@ -373,11 +399,11 @@ IdpMetadataParser by its Entity Id value:
373
399
  )
374
400
  ```
375
401
 
376
- ### Retrieve one Entity Descriptor with an specific binding and nameid format when several are available
402
+ ### Retrieve one Entity Descriptor with a specific binding and nameid format when several are available
377
403
 
378
- If the Metadata contains several bindings and nameids, the relevant ones
379
- also can be specified when retrieving the settings from the IdpMetadataParser
380
- by the values of binding and nameid:
404
+ If the metadata contains multiple bindings and NameID formats, the relevant ones
405
+ can be specified when retrieving the settings from the IdpMetadataParser
406
+ by the values of binding and NameID:
381
407
 
382
408
  ```ruby
383
409
  validate_cert = true
@@ -441,13 +467,13 @@ if valid
441
467
  entity_id: "<entity_id_of_the_entity_to_be_retrieved>"
442
468
  )
443
469
  else
444
- print "Metadata Signarture failed to be verified with the cert provided"
470
+ print "Metadata Signature failed to be verified with the cert provided"
445
471
  end
446
472
  ```
447
473
 
448
474
  ## Retrieving Attributes
449
475
 
450
- If you are using `saml:AttributeStatement` to transfer data like the username, you can access all the attributes through `response.attributes`. It contains all the `saml:AttributeStatement`s with its 'Name' as an indifferent key and one or more `saml:AttributeValue`s as values. The value returned depends on the value of the
476
+ If you are using `saml:AttributeStatement` to transfer data, such as the username, you can access all the attributes through `response.attributes`. It contains all the `saml:AttributeStatement`s with its 'Name' as an indifferent key and one or more `saml:AttributeValue`s as values. The value returned depends on the value of the
451
477
  `single_value_compatibility` (when activated, only the first value is returned)
452
478
 
453
479
  ```ruby
@@ -670,7 +696,7 @@ Next, you may specify the specific SP SAML messages you would like to sign:
670
696
  settings.security[:logout_responses_signed] = true # Enable signature on Logout Response
671
697
  ```
672
698
 
673
- Signatures will be handled automatically for both `HTTP-Redirect` and `HTTP-Redirect` Binding.
699
+ Signatures will be handled automatically for both `HTTP-Redirect` and `HTTP-POST` Binding.
674
700
  Note that the RelayState parameter is used when creating the Signature on the `HTTP-Redirect` Binding.
675
701
  Remember to provide it to the Signature builder if you are sending a `GET RelayState` parameter or the
676
702
  signature validation process will fail at the Identity Provider.
@@ -762,7 +788,7 @@ Note the following:
762
788
  #### Audience Validation
763
789
 
764
790
  A service provider should only consider a SAML response valid if the IdP includes an <AudienceRestriction>
765
- element containting an <Audience> element that uniquely identifies the service provider. Unless you specify
791
+ element containing an <Audience> element that uniquely identifies the service provider. Unless you specify
766
792
  the `skip_audience` option, Ruby SAML will validate that each SAML response includes an <Audience> element
767
793
  whose contents matches `settings.sp_entity_id`.
768
794
 
@@ -940,7 +966,7 @@ end
940
966
 
941
967
  ## Attribute Service
942
968
 
943
- To request attributes from the IdP the SP needs to provide an attribute service within it's metadata and reference the index in the assertion.
969
+ To request attributes from the IdP the SP must provide an attribute service within its metadata and reference the index in the assertion.
944
970
 
945
971
  ```ruby
946
972
  settings = OneLogin::RubySaml::Settings.new
@@ -957,7 +983,7 @@ The `attribute_value` option additionally accepts an array of possible values.
957
983
 
958
984
  ## Custom Metadata Fields
959
985
 
960
- Some IdPs may require to add SPs to add additional fields (Organization, ContactPerson, etc.)
986
+ Some IdPs may require SPs to add additional fields (Organization, ContactPerson, etc.)
961
987
  into the SP metadata. This can be achieved by extending the `OneLogin::RubySaml::Metadata`
962
988
  class and overriding the `#add_extras` method as per the following example:
963
989
 
data/UPGRADING.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Ruby SAML Migration Guide
2
2
 
3
+ ## Updating from 1.17.x to 1.18.0
4
+
5
+ Version `1.18.0` changes the way the toolkit validates SAML signatures. There is a new order
6
+ how validation happens in the toolkit and also the toolkit by default will check malformed doc
7
+ when parsing a SAML Message (`settings.check_malformed_doc`).
8
+
9
+ The SignedDocument class defined at xml_security.rb experienced several changes.
10
+ We don't expect compatibilty issues if you use the main methods offered by ruby-saml, but if you use a fork or customized usage, is possible that you need to adapt your code.
11
+
3
12
  ## Updating from 1.12.x to 1.13.0
4
13
 
5
14
  Version `1.13.0` adds `settings.idp_sso_service_binding` and `settings.idp_slo_service_binding`, and
@@ -64,7 +64,7 @@ module OneLogin
64
64
  request_doc = create_authentication_xml_doc(settings)
65
65
  request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
66
66
 
67
- request = ""
67
+ request = "".dup
68
68
  request_doc.write(request)
69
69
 
70
70
  Logging.debug "Created AuthnRequest: #{request}"
@@ -384,14 +384,12 @@ module OneLogin
384
384
  # @return [String|nil] the fingerpint of the X509Certificate if it exists
385
385
  #
386
386
  def fingerprint(certificate, fingerprint_algorithm = XMLSecurity::Document::SHA1)
387
- @fingerprint ||= begin
388
- return unless certificate
387
+ return unless certificate
389
388
 
390
- cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate))
389
+ cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate))
391
390
 
392
- fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(fingerprint_algorithm).new
393
- fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":")
394
- end
391
+ fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(fingerprint_algorithm).new
392
+ fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":")
395
393
  end
396
394
 
397
395
  # @return [Array] the names of all SAML attributes if any exist
@@ -61,7 +61,7 @@ module OneLogin
61
61
  request_doc = create_logout_request_xml_doc(settings)
62
62
  request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
63
63
 
64
- request = ""
64
+ request = "".dup
65
65
  request_doc.write(request)
66
66
 
67
67
  Logging.debug "Created SLO Logout Request: #{request}"
@@ -150,7 +150,8 @@ module OneLogin
150
150
  # @raise [ValidationError] if soft == false and validation fails
151
151
  #
152
152
  def validate_structure
153
- unless valid_saml?(document, soft)
153
+ check_malformed_doc = check_malformed_doc?(settings)
154
+ unless valid_saml?(document, soft, check_malformed_doc)
154
155
  return append_error("Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd")
155
156
  end
156
157
 
@@ -145,7 +145,7 @@ module OneLogin
145
145
  end
146
146
 
147
147
  def output_xml(meta_doc, pretty_print)
148
- ret = ''
148
+ ret = ''.dup
149
149
 
150
150
  # pretty print the XML so IdP administrators can easily see what the SP supports
151
151
  if pretty_print
@@ -17,6 +17,10 @@ module OneLogin
17
17
  PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
18
18
  DSIG = "http://www.w3.org/2000/09/xmldsig#"
19
19
  XENC = "http://www.w3.org/2001/04/xmlenc#"
20
+ SAML_NAMESPACES = {
21
+ "p" => PROTOCOL,
22
+ "a" => ASSERTION
23
+ }.freeze
20
24
 
21
25
  # TODO: Settings should probably be initialized too... WDYT?
22
26
 
@@ -198,6 +202,27 @@ module OneLogin
198
202
  end
199
203
  end
200
204
 
205
+ # Gets the AuthnInstant from the AuthnStatement.
206
+ # Could be used to require re-authentication if a long time has passed
207
+ # since the last user authentication.
208
+ # @return [String] AuthnInstant value
209
+ #
210
+ def authn_instant
211
+ @authn_instant ||= begin
212
+ node = xpath_first_from_signed_assertion('/a:AuthnStatement')
213
+ node.nil? ? nil : node.attributes['AuthnInstant']
214
+ end
215
+ end
216
+
217
+ # Gets the AuthnContextClassRef from the AuthnStatement
218
+ # Could be used to require re-authentication if the assertion
219
+ # did not met the requested authentication context class.
220
+ # @return [String] AuthnContextClassRef value
221
+ #
222
+ def authn_context_class_ref
223
+ @authn_context_class_ref ||= Utils.element_text(xpath_first_from_signed_assertion('/a:AuthnStatement/a:AuthnContext/a:AuthnContextClassRef'))
224
+ end
225
+
201
226
  # Checks if the Status has the "Success" code
202
227
  # @return [Boolean] True if the StatusCode is Sucess
203
228
  #
@@ -282,7 +307,7 @@ module OneLogin
282
307
  issuer_response_nodes = REXML::XPath.match(
283
308
  document,
284
309
  "/p:Response/a:Issuer",
285
- { "p" => PROTOCOL, "a" => ASSERTION }
310
+ SAML_NAMESPACES
286
311
  )
287
312
 
288
313
  unless issuer_response_nodes.size == 1
@@ -349,7 +374,7 @@ module OneLogin
349
374
  ! REXML::XPath.first(
350
375
  document,
351
376
  "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
352
- { "p" => PROTOCOL, "a" => ASSERTION }
377
+ SAML_NAMESPACES
353
378
  ).nil?
354
379
  end
355
380
 
@@ -380,9 +405,9 @@ module OneLogin
380
405
  :validate_id,
381
406
  :validate_success_status,
382
407
  :validate_num_assertion,
383
- :validate_no_duplicated_attributes,
384
408
  :validate_signed_elements,
385
409
  :validate_structure,
410
+ :validate_no_duplicated_attributes,
386
411
  :validate_in_response_to,
387
412
  :validate_one_conditions,
388
413
  :validate_conditions,
@@ -423,12 +448,14 @@ module OneLogin
423
448
  #
424
449
  def validate_structure
425
450
  structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
426
- unless valid_saml?(document, soft)
451
+
452
+ check_malformed_doc = check_malformed_doc_enabled?
453
+ unless valid_saml?(document, soft, check_malformed_doc)
427
454
  return append_error(structure_error_msg)
428
455
  end
429
456
 
430
457
  unless decrypted_document.nil?
431
- unless valid_saml?(decrypted_document, soft)
458
+ unless valid_saml?(decrypted_document, soft, check_malformed_doc)
432
459
  return append_error(structure_error_msg)
433
460
  end
434
461
  end
@@ -812,7 +839,7 @@ module OneLogin
812
839
 
813
840
  unless settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
814
841
  if name_id_spnamequalifier != settings.sp_entity_id
815
- return append_error("The SPNameQualifier value mistmatch the SP entityID value.")
842
+ return append_error("SPNameQualifier value does not match the SP entityID value.")
816
843
  end
817
844
  end
818
845
  end
@@ -820,6 +847,25 @@ module OneLogin
820
847
  true
821
848
  end
822
849
 
850
+ def doc_to_validate
851
+ # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response
852
+ # otherwise, review if the decrypted assertion contains a signature
853
+ sig_elements = REXML::XPath.match(
854
+ document,
855
+ "/p:Response[@ID=$id]/ds:Signature",
856
+ { "p" => PROTOCOL, "ds" => DSIG },
857
+ { 'id' => document.signed_element_id }
858
+ )
859
+
860
+ use_original = sig_elements.size == 1 || decrypted_document.nil?
861
+ doc = use_original ? document : decrypted_document
862
+ if !doc.processed
863
+ doc.cache_referenced_xml(@soft, check_malformed_doc_enabled?)
864
+ end
865
+
866
+ return doc
867
+ end
868
+
823
869
  # Validates the Signature
824
870
  # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
825
871
  # @raise [ValidationError] if soft == false and validation fails
@@ -827,8 +873,8 @@ module OneLogin
827
873
  def validate_signature
828
874
  error_msg = "Invalid Signature on SAML Response"
829
875
 
830
- # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response
831
- # otherwise, review if the decrypted assertion contains a signature
876
+ doc = doc_to_validate
877
+
832
878
  sig_elements = REXML::XPath.match(
833
879
  document,
834
880
  "/p:Response[@ID=$id]/ds:Signature",
@@ -836,15 +882,12 @@ module OneLogin
836
882
  { 'id' => document.signed_element_id }
837
883
  )
838
884
 
839
- use_original = sig_elements.size == 1 || decrypted_document.nil?
840
- doc = use_original ? document : decrypted_document
841
-
842
- # Check signature nodes
885
+ # Check signature node inside assertion
843
886
  if sig_elements.nil? || sig_elements.size == 0
844
887
  sig_elements = REXML::XPath.match(
845
888
  doc,
846
889
  "/p:Response/a:Assertion[@ID=$id]/ds:Signature",
847
- {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG},
890
+ SAML_NAMESPACES.merge({"ds"=>DSIG}),
848
891
  { 'id' => doc.signed_element_id }
849
892
  )
850
893
  end
@@ -922,24 +965,47 @@ module OneLogin
922
965
  end
923
966
  end
924
967
 
968
+ def get_cached_signed_assertion
969
+ xml = doc_to_validate.referenced_xml
970
+ empty_doc = REXML::Document.new
971
+
972
+ return empty_doc if xml.nil? # when no signature/reference is found, return empty document
973
+
974
+ root = REXML::Document.new(xml).root
975
+
976
+ if root.attributes["ID"] != doc_to_validate.signed_element_id
977
+ return empty_doc
978
+ end
979
+
980
+ assertion = empty_doc
981
+ if root.name == "Response"
982
+ if REXML::XPath.first(root, "a:Assertion", {"a" => ASSERTION})
983
+ assertion = REXML::XPath.first(root, "a:Assertion", {"a" => ASSERTION})
984
+ elsif REXML::XPath.first(root, "a:EncryptedAssertion", {"a" => ASSERTION})
985
+ assertion = decrypt_assertion(REXML::XPath.first(root, "a:EncryptedAssertion", {"a" => ASSERTION}))
986
+ end
987
+ elsif root.name == "Assertion"
988
+ assertion = root
989
+ end
990
+
991
+ assertion
992
+ end
993
+
994
+ def signed_assertion
995
+ @signed_assertion ||= get_cached_signed_assertion
996
+ end
997
+
925
998
  # Extracts the first appearance that matchs the subelt (pattern)
926
999
  # Search on any Assertion that is signed, or has a Response parent signed
927
1000
  # @param subelt [String] The XPath pattern
928
1001
  # @return [REXML::Element | nil] If any matches, return the Element
929
1002
  #
930
1003
  def xpath_first_from_signed_assertion(subelt=nil)
931
- doc = decrypted_document.nil? ? document : decrypted_document
1004
+ doc = signed_assertion
932
1005
  node = REXML::XPath.first(
933
1006
  doc,
934
- "/p:Response/a:Assertion[@ID=$id]#{subelt}",
935
- { "p" => PROTOCOL, "a" => ASSERTION },
936
- { 'id' => doc.signed_element_id }
937
- )
938
- node ||= REXML::XPath.first(
939
- doc,
940
- "/p:Response[@ID=$id]/a:Assertion#{subelt}",
941
- { "p" => PROTOCOL, "a" => ASSERTION },
942
- { 'id' => doc.signed_element_id }
1007
+ "./#{subelt}",
1008
+ SAML_NAMESPACES
943
1009
  )
944
1010
  node
945
1011
  end
@@ -950,19 +1016,13 @@ module OneLogin
950
1016
  # @return [Array of REXML::Element] Return all matches
951
1017
  #
952
1018
  def xpath_from_signed_assertion(subelt=nil)
953
- doc = decrypted_document.nil? ? document : decrypted_document
1019
+ doc = signed_assertion
954
1020
  node = REXML::XPath.match(
955
1021
  doc,
956
- "/p:Response/a:Assertion[@ID=$id]#{subelt}",
957
- { "p" => PROTOCOL, "a" => ASSERTION },
958
- { 'id' => doc.signed_element_id }
1022
+ "./#{subelt}",
1023
+ SAML_NAMESPACES
959
1024
  )
960
- node.concat( REXML::XPath.match(
961
- doc,
962
- "/p:Response[@ID=$id]/a:Assertion#{subelt}",
963
- { "p" => PROTOCOL, "a" => ASSERTION },
964
- { 'id' => doc.signed_element_id }
965
- ))
1025
+ node
966
1026
  end
967
1027
 
968
1028
  # Generates the decrypted_document
@@ -996,7 +1056,7 @@ module OneLogin
996
1056
  encrypted_assertion_node = REXML::XPath.first(
997
1057
  document_copy,
998
1058
  "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
999
- { "p" => PROTOCOL, "a" => ASSERTION }
1059
+ SAML_NAMESPACES
1000
1060
  )
1001
1061
  response_node.add(decrypt_assertion(encrypted_assertion_node))
1002
1062
  encrypted_assertion_node.remove
@@ -1066,6 +1126,10 @@ module OneLogin
1066
1126
  Time.parse(node.attributes[attribute])
1067
1127
  end
1068
1128
  end
1129
+
1130
+ def check_malformed_doc_enabled?
1131
+ check_malformed_doc?(settings)
1132
+ end
1069
1133
  end
1070
1134
  end
1071
1135
  end
@@ -19,15 +19,13 @@ module OneLogin
19
19
  PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol".freeze
20
20
 
21
21
  BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z)
22
- @@mutex = Mutex.new
23
22
 
24
23
  # @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema
25
24
  #
26
25
  def self.schema
27
- @@mutex.synchronize do
28
- Dir.chdir(File.expand_path("../../../schemas", __FILE__)) do
29
- ::Nokogiri::XML::Schema(File.read("saml-schema-protocol-2.0.xsd"))
30
- end
26
+ path = File.expand_path("../../../schemas/saml-schema-protocol-2.0.xsd", __FILE__)
27
+ File.open(path) do |file|
28
+ ::Nokogiri::XML::Schema(file)
31
29
  end
32
30
  end
33
31
 
@@ -60,14 +58,13 @@ module OneLogin
60
58
  # Validates the SAML Message against the specified schema.
61
59
  # @param document [REXML::Document] The message that will be validated
62
60
  # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the message is invalid or not)
61
+ # @param check_malformed_doc [Boolean] check_malformed_doc Enable or Disable the check for malformed XML
63
62
  # @return [Boolean] True if the XML is valid, otherwise False, if soft=True
64
63
  # @raise [ValidationError] if soft == false and validation fails
65
64
  #
66
- def valid_saml?(document, soft = true)
65
+ def valid_saml?(document, soft = true, check_malformed_doc = true)
67
66
  begin
68
- xml = Nokogiri::XML(document.to_s) do |config|
69
- config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
70
- end
67
+ xml = XMLSecurity::BaseDocument.safe_load_xml(document, check_malformed_doc)
71
68
  rescue StandardError => error
72
69
  return false if soft
73
70
  raise ValidationError.new("XML load failed: #{error.message}")
@@ -83,6 +80,7 @@ module OneLogin
83
80
 
84
81
  # Base64 decode and try also to inflate a SAML Message
85
82
  # @param saml [String] The deflated and encoded SAML Message
83
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
86
84
  # @return [String] The plain SAML Message
87
85
  #
88
86
  def decode_raw_saml(saml, settings = nil)
@@ -95,10 +93,16 @@ module OneLogin
95
93
 
96
94
  decoded = decode(saml)
97
95
  begin
98
- inflate(decoded)
96
+ message = inflate(decoded)
99
97
  rescue
100
- decoded
98
+ message = decoded
99
+ end
100
+
101
+ if message.bytesize > settings.message_max_bytesize
102
+ raise ValidationError.new("SAML Message exceeds " + settings.message_max_bytesize.to_s + " bytes, so was rejected")
101
103
  end
104
+
105
+ message
102
106
  end
103
107
 
104
108
  # Deflate, base64 encode and url-encode a SAML Message (To be used in the HTTP-redirect binding)
@@ -155,6 +159,12 @@ module OneLogin
155
159
  def deflate(inflated)
156
160
  Zlib::Deflate.deflate(inflated, 9)[2..-5]
157
161
  end
162
+
163
+ def check_malformed_doc?(settings)
164
+ default_value = OneLogin::RubySaml::Settings::DEFAULTS[:check_malformed_doc]
165
+
166
+ settings.nil? ? default_value : settings.check_malformed_doc
167
+ end
158
168
  end
159
169
  end
160
170
  end
@@ -55,6 +55,7 @@ module OneLogin
55
55
  attr_accessor :compress_response
56
56
  attr_accessor :double_quote_xml_attribute_values
57
57
  attr_accessor :message_max_bytesize
58
+ attr_accessor :check_malformed_doc
58
59
  attr_accessor :passive
59
60
  attr_reader :protocol_binding
60
61
  attr_accessor :attributes_index
@@ -281,7 +282,9 @@ module OneLogin
281
282
  :compress_response => true,
282
283
  :message_max_bytesize => 250000,
283
284
  :soft => true,
285
+ :check_malformed_doc => true,
284
286
  :double_quote_xml_attribute_values => false,
287
+
285
288
  :security => {
286
289
  :authn_requests_signed => false,
287
290
  :logout_requests_signed => false,
@@ -238,7 +238,8 @@ module OneLogin
238
238
  # @raise [ValidationError] if soft == false and validation fails
239
239
  #
240
240
  def validate_structure
241
- unless valid_saml?(document, soft)
241
+ check_malformed_doc = check_malformed_doc?(settings)
242
+ unless valid_saml?(document, soft, check_malformed_doc)
242
243
  return append_error("Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd")
243
244
  end
244
245
 
@@ -70,7 +70,7 @@ module OneLogin
70
70
  response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code)
71
71
  response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
72
72
 
73
- response = ""
73
+ response = "".dup
74
74
  response_doc.write(response)
75
75
 
76
76
  Logging.debug "Created SLO Logout Response: #{response}"
@@ -32,7 +32,9 @@ module OneLogin
32
32
  (\d+)W # 8: Weeks
33
33
  )
34
34
  $)x.freeze
35
+
35
36
  UUID_PREFIX = '_'
37
+ @@prefix = '_'
36
38
 
37
39
  # Checks if the x509 cert provided is expired.
38
40
  #
@@ -252,6 +254,8 @@ module OneLogin
252
254
  # @param status_message [Strig] StatusMessage value
253
255
  # @return [String] The status error message
254
256
  def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil)
257
+ error_msg = error_msg.dup
258
+
255
259
  unless raw_status_code.nil?
256
260
  if raw_status_code.include? "|"
257
261
  status_codes = raw_status_code.split(' | ')
@@ -400,11 +404,15 @@ module OneLogin
400
404
  end
401
405
 
402
406
  def self.set_prefix(value)
403
- UUID_PREFIX.replace value
407
+ @@prefix = value
408
+ end
409
+
410
+ def self.prefix
411
+ @@prefix
404
412
  end
405
413
 
406
414
  def self.uuid
407
- "#{UUID_PREFIX}" + (RUBY_VERSION < '1.9' ? "#{@@uuid_generator.generate}" : "#{SecureRandom.uuid}")
415
+ "#{prefix}" + (RUBY_VERSION < '1.9' ? "#{@@uuid_generator.generate}" : "#{SecureRandom.uuid}")
408
416
  end
409
417
 
410
418
  # Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed,
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '1.17.0'
3
+ VERSION = '1.18.0'
4
4
  end
5
5
  end
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,66 +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)
311
- noko_sig_element.remove
368
+ @cached_signed_info = noko_signed_info_element.canonicalize(canon_algorithm)
312
369
 
313
- # get signed info
314
- signed_info_element = REXML::XPath.first(
315
- sig_element,
316
- "./ds:SignedInfo",
317
- { "ds" => DSIG }
318
- )
370
+ ### Now get the @referenced_xml to use?
371
+ rexml_signed_info = REXML::Document.new(@cached_signed_info.to_s).root
372
+
373
+ noko_sig_element.remove
319
374
 
320
375
  # get inclusive namespaces
321
376
  inclusive_namespaces = extract_inclusive_namespaces
322
377
 
323
378
  # check digests
324
- ref = REXML::XPath.first(signed_info_element, "./ds:Reference", {"ds"=>DSIG})
325
-
326
- reference_nodes = document.xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
379
+ @ref = REXML::XPath.first(rexml_signed_info, "./ds:Reference", {"ds"=>DSIG})
380
+ return if @ref.nil?
327
381
 
328
- if reference_nodes.length > 1 # ensures no elements with same ID to prevent signature wrapping attack.
329
- return append_error("Digest mismatch. Duplicated ID found", soft)
330
- end
382
+ reference_nodes = nokogiri_document.xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
331
383
 
332
384
  hashed_element = reference_nodes[0]
385
+ return if hashed_element.nil?
333
386
 
334
387
  canon_algorithm = canon_algorithm REXML::XPath.first(
335
- signed_info_element,
388
+ rexml_signed_info,
336
389
  './ds:CanonicalizationMethod',
337
390
  { "ds" => DSIG }
338
391
  )
339
392
 
340
- 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?
341
408
 
342
- 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)
343
412
 
344
413
  digest_algorithm = algorithm(REXML::XPath.first(
345
- ref,
414
+ @ref,
346
415
  "./ds:DigestMethod",
347
416
  { "ds" => DSIG }
348
417
  ))
349
- hash = digest_algorithm.digest(canon_hashed_element)
418
+ hash = digest_algorithm.digest(@referenced_xml)
350
419
  encoded_digest_value = REXML::XPath.first(
351
- ref,
420
+ @ref,
352
421
  "./ds:DigestValue",
353
422
  { "ds" => DSIG }
354
423
  )
355
- 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)
356
426
 
357
- unless digests_match?(hash, digest_value)
427
+ # Compare the computed "hash" with the "signed" hash
428
+ unless hash && hash == digest_value
358
429
  return append_error("Digest mismatch", soft)
359
430
  end
360
431
 
361
- # get certificate object
362
- cert_text = Base64.decode64(base64_cert)
363
- cert = OpenSSL::X509::Certificate.new(cert_text)
364
-
365
432
  # verify signature
366
- unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
433
+ unless cert.public_key.verify(@signature_algorithm.new, @signature, @cached_signed_info)
367
434
  return append_error("Key validation error", soft)
368
435
  end
369
436
 
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.17.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: 2024-09-10 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