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 +4 -4
- data/CHANGELOG.md +16 -7
- data/README.md +51 -25
- data/UPGRADING.md +9 -0
- data/lib/onelogin/ruby-saml/authrequest.rb +1 -1
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +4 -6
- data/lib/onelogin/ruby-saml/logoutrequest.rb +1 -1
- data/lib/onelogin/ruby-saml/logoutresponse.rb +2 -1
- data/lib/onelogin/ruby-saml/metadata.rb +1 -1
- data/lib/onelogin/ruby-saml/response.rb +98 -34
- data/lib/onelogin/ruby-saml/saml_message.rb +21 -11
- data/lib/onelogin/ruby-saml/settings.rb +3 -0
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +2 -1
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +1 -1
- data/lib/onelogin/ruby-saml/utils.rb +10 -2
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/xml_security.rb +110 -43
- data/ruby-saml.gemspec +6 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 59cce47afe1159fc5674f2eaf8f0392d12df280cc97fa2a4ccd7926daf989445
|
4
|
+
data.tar.gz: 30a49c237e7a88328745b788d62e9ea92e87342fc571d20e002eb4feddd964ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
[](https://badge.fury.io/rb/ruby-saml)
|
5
5
|
[](https://badge.fury.io/gh/SAML-Toolkits%2Fruby-saml)   
|
6
6
|
|
7
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
62
|
-
delegates this functionality to other users. In this case, extra
|
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.
|
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,
|
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
|
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
|
402
|
+
### Retrieve one Entity Descriptor with a specific binding and nameid format when several are available
|
377
403
|
|
378
|
-
If the
|
379
|
-
|
380
|
-
by the values of binding and
|
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
|
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
|
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-
|
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
|
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
|
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
|
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
|
-
|
388
|
-
return unless certificate
|
387
|
+
return unless certificate
|
389
388
|
|
390
|
-
|
389
|
+
cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate))
|
391
390
|
|
392
|
-
|
393
|
-
|
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
|
-
|
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
|
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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("
|
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
|
-
|
831
|
-
|
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
|
-
|
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
|
-
{"
|
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 =
|
1004
|
+
doc = signed_assertion
|
932
1005
|
node = REXML::XPath.first(
|
933
1006
|
doc,
|
934
|
-
"
|
935
|
-
|
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 =
|
1019
|
+
doc = signed_assertion
|
954
1020
|
node = REXML::XPath.match(
|
955
1021
|
doc,
|
956
|
-
"
|
957
|
-
|
958
|
-
{ 'id' => doc.signed_element_id }
|
1022
|
+
"./#{subelt}",
|
1023
|
+
SAML_NAMESPACES
|
959
1024
|
)
|
960
|
-
node
|
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
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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 =
|
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
|
-
|
96
|
+
message = inflate(decoded)
|
99
97
|
rescue
|
100
|
-
|
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
|
-
|
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
|
-
|
407
|
+
@@prefix = value
|
408
|
+
end
|
409
|
+
|
410
|
+
def self.prefix
|
411
|
+
@@prefix
|
404
412
|
end
|
405
413
|
|
406
414
|
def self.uuid
|
407
|
-
"#{
|
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,
|
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,66 +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
|
-
|
311
|
-
noko_sig_element.remove
|
368
|
+
@cached_signed_info = noko_signed_info_element.canonicalize(canon_algorithm)
|
312
369
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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,
|
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.
|
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
|