ruby-saml 1.15.0 → 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ed3b0ab8cb9f9fd8b4e23b34f8ad06fd1e6c6a13d885d34c2f7b385297783b3
4
- data.tar.gz: 757dccd6c1418f1128a69c7fdcd95cdd8d8bcac697ae750edb92f058aee50c7f
3
+ metadata.gz: 188af740c1b9627be73d3a4cbd8261316773d3b3da0b74b0ef49b5d1c9c04f02
4
+ data.tar.gz: 85cb561ba597924b7037be197dac6fd175c1a626969829e170bb44d8e8a1a50d
5
5
  SHA512:
6
- metadata.gz: 9cc12490c6b57281677f1db5a5a804c07a9b97f57a86d3b5676b79b36ca28bd7d9971ed3db893b730ff09ab6c03374600d87118949440c4271293bc48333b3d6
7
- data.tar.gz: 4c330a53de476f479a22dbe07b18f51e0ebe1dcde834eac9cb65678b8767094e365a0ea043a5e4b09d1cabfa6a8f0c4287f29fffe6e53328b70572a64cefcd04
6
+ metadata.gz: 0b154ce0a94f1f1b525179d33f8a98d5422cabafe527f32104646135ae0a9218064638639b7ec5735de1dda8ef55faa5571f22f563d57f44e040a81b6116b5a1
7
+ data.tar.gz: 78ea0021299530423e28935782b851894ed9740c1e93c610575518532d22bcc20829dbe26b0569f38ad21894cc145b3d785c75765c8bd0dde6e078598d864545
@@ -0,0 +1,3 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [SAML-Toolkits]
@@ -8,11 +8,57 @@ jobs:
8
8
  strategy:
9
9
  fail-fast: false
10
10
  matrix:
11
- os: [ubuntu-20.04, macos-latest]
12
- ruby-version: [2.1.9, 2.2.10, 2.3.8, 2.4.6, 2.5.8, 2.6.6, 2.7.2, 3.0.1, 3.1, 3.2, jruby-9.1.17.0, jruby-9.2.17.0, jruby-9.3.2.0, jruby-9.4.0.0, truffleruby]
11
+ os:
12
+ - ubuntu-20.04
13
+ - macos-latest
14
+ - windows-latest
15
+ ruby-version:
16
+ - 2.1
17
+ - 2.2
18
+ - 2.3
19
+ - 2.4
20
+ - 2.5
21
+ - 2.6
22
+ - 2.7
23
+ - 3.0
24
+ - 3.1
25
+ - 3.2
26
+ - 3.3
27
+ - jruby-9.1
28
+ - jruby-9.2
29
+ - jruby-9.3
30
+ - jruby-9.4
31
+ - truffleruby
32
+ exclude:
33
+ - os: macos-latest
34
+ ruby-version: 2.1
35
+ - os: macos-latest
36
+ ruby-version: 2.2
37
+ - os: macos-latest
38
+ ruby-version: 2.3
39
+ - os: macos-latest
40
+ ruby-version: 2.4
41
+ - os: macos-latest
42
+ ruby-version: 2.5
43
+ - os: macos-latest
44
+ ruby-version: jruby-9.1
45
+ - os: macos-latest
46
+ ruby-version: jruby-9.2
47
+ - os: windows-latest
48
+ ruby-version: 2.1
49
+ - os: windows-latest
50
+ ruby-version: jruby-9.1
51
+ - os: windows-latest
52
+ ruby-version: jruby-9.2
53
+ - os: windows-latest
54
+ ruby-version: jruby-9.3
55
+ - os: windows-latest
56
+ ruby-version: jruby-9.4
57
+ - os: windows-latest
58
+ ruby-version: truffleruby
13
59
  runs-on: ${{ matrix.os }}
14
60
  steps:
15
- - uses: actions/checkout@v2
61
+ - uses: actions/checkout@v4
16
62
  - name: Set up Ruby ${{ matrix.ruby-version }}
17
63
  uses: ruby/setup-ruby@v1
18
64
  with:
data/CHANGELOG.md CHANGED
@@ -1,8 +1,22 @@
1
1
  # Ruby SAML Changelog
2
+
3
+ ### 1.17.0 (Sep 10, 2024)
4
+ * Fix for critical vulnerability CVE-2024-45409: SAML authentication bypass via Incorrect XPath selector
5
+ * [#687](https://github.com/SAML-Toolkits/ruby-saml/pull/687) Add CI coverage for Ruby 3.3 and Windows.
6
+ * [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Add `Settings#sp_cert_multi` paramter to facilitate SP certificate and key rotation.
7
+ * [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Support multiple simultaneous SP decryption keys via `Settings#sp_cert_multi` parameter.
8
+ * [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Deprecate `Settings#certificate_new` parameter.
9
+ * [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) `:check_sp_cert_expiration` will use the first non-expired certificate/key when signing/decrypting. It will raise an error only if there are no valid certificates/keys.
10
+ * [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) `:check_sp_cert_expiration` now validates the certificate `not_before` condition; previously it was only validating `not_after`.
11
+ * [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) `:check_sp_cert_expiration` now causes the generated SP metadata to exclude any inactive/expired certificates.
12
+
13
+ ### 1.16.0 (Oct 09, 2023)
14
+ * [#671](https://github.com/SAML-Toolkits/ruby-saml/pull/671) Add support on LogoutRequest with Encrypted NameID
15
+
2
16
  ### 1.15.0 (Jan 04, 2023)
3
17
  * [#650](https://github.com/SAML-Toolkits/ruby-saml/pull/650) Replace strip! by strip on compute_digest method
4
18
  * [#638](https://github.com/SAML-Toolkits/ruby-saml/pull/638) Fix dateTime format for the validUntil attribute of the generated metadata
5
- * [#576](https://github.com/SAML-Toolkits/ruby-saml/pull/576) Support idp cert multi with string keys
19
+ * [#576](https://github.com/SAML-Toolkits/ruby-saml/pull/576) Support `Settings#idp_cert_multi` with string keys
6
20
  * [#567](https://github.com/SAML-Toolkits/ruby-saml/pull/567) Improve Code quality
7
21
  * Add info about new repo, new maintainer, new security contact
8
22
  * Fix tests, Adjust dependencies, Add ruby 3.2 and new jruby versions tests to the CI. Add coveralls support
@@ -26,6 +40,9 @@
26
40
  * Add warning about the use of IdpMetadataParser class and SSRF
27
41
  * CI: Migrate from Travis to Github Actions
28
42
 
43
+ ### 1.12.3 (Sep 10, 2024)
44
+ * Fix for critical vulnerability CVE-2024-45409: SAML authentication bypass via Incorrect XPath selector
45
+
29
46
  ### 1.12.2 (Apr 08, 2021)
30
47
  * [#575](https://github.com/onelogin/ruby-saml/pull/575) Fix SloLogoutresponse bug on LogoutRequest
31
48
 
@@ -47,7 +64,7 @@
47
64
  * Support Process Transform
48
65
  * Raise SettingError if invoking an action with no endpoint defined on the settings
49
66
  * Made IdpMetadataParser more extensible for subclasses
50
- *[#548](https://github.com/onelogin/ruby-saml/pull/548) Add :skip_audience option
67
+ * [#548](https://github.com/onelogin/ruby-saml/pull/548) Add :skip_audience option
51
68
  * [#555](https://github.com/onelogin/ruby-saml/pull/555) Define 'soft' variable to prevent exception when doc cert is invalid
52
69
  * Improve documentation
53
70
 
data/README.md CHANGED
@@ -1,10 +1,14 @@
1
1
  # Ruby SAML
2
- [![Build Status](https://github.com/onelogin/ruby-saml/actions/workflows/test.yml/badge.svg?query=branch%3Amaster)](https://github.com/onelogin/ruby-saml/actions/workflows/test.yml?query=branch%3Amaster)
3
- [![Coverage Status](https://coveralls.io/repos/onelogin/ruby-saml/badge.svg?branch=master)](https://coveralls.io/r/onelogin/ruby-saml?branch=master)
2
+ [![ruby-saml CI](https://github.com/SAML-Toolkits/ruby-saml/actions/workflows/test.yml/badge.svg)](https://github.com/SAML-Toolkits/ruby-saml/actions/workflows/test.yml)
3
+ [![Coverage Status](https://coveralls.io/repos/github/SAML-Toolkits/ruby-saml/badge.svg?branch=master)](https://coveralls.io/github/SAML-Toolkits/ruby-saml?branch=master)
4
+ [![Rubygem Version](https://badge.fury.io/rb/ruby-saml.svg)](https://badge.fury.io/rb/ruby-saml)
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)
4
6
 
5
7
  Ruby SAML minor and tiny versions may introduce breaking changes. Please read
6
8
  [UPGRADING.md](UPGRADING.md) for guidance on upgrading to new Ruby SAML versions.
7
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)
11
+
8
12
  ## Overview
9
13
 
10
14
  The Ruby SAML library is for implementing the client side of a SAML authorization,
@@ -14,32 +18,16 @@ requests from identity providers.
14
18
  SAML authorization is a two step process and you are expected to implement support for both.
15
19
 
16
20
  We created a demo project for Rails 4 that uses the latest version of this library:
17
- [ruby-saml-example](https://github.com/saml-toolkit/ruby-saml-example)
21
+ [ruby-saml-example](https://github.com/saml-toolkits/ruby-saml-example)
18
22
 
19
23
  ### Supported Ruby Versions
20
24
 
21
25
  The following Ruby versions are covered by CI testing:
22
26
 
23
- * 2.1.x
24
- * 2.2.x
25
- * 2.3.x
26
- * 2.4.x
27
- * 2.5.x
28
- * 2.6.x
29
- * 2.7.x
30
- * 3.0.x
31
- * JRuby 9.1.x
32
- * JRuby 9.2.x
27
+ * Ruby (MRI) 2.1 to 3.3
28
+ * JRuby 9.1 to 9.4
33
29
  * TruffleRuby (latest)
34
30
 
35
- In addition, the following may work but are untested:
36
-
37
- * 1.8.7
38
- * 1.9.x
39
- * 2.0.x
40
- * JRuby 1.7.x
41
- * JRuby 9.0.x
42
-
43
31
  ## Adding Features, Pull Requests
44
32
 
45
33
  * Fork the repository
@@ -385,6 +373,27 @@ IdpMetadataParser by its Entity Id value:
385
373
  )
386
374
  ```
387
375
 
376
+ ### Retrieve one Entity Descriptor with an specific binding and nameid format when several are available
377
+
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:
381
+
382
+ ```ruby
383
+ validate_cert = true
384
+ options = {
385
+ entity_id: "http//example.com/target/entity",
386
+ name_id_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
387
+ sso_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
388
+ slo_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
389
+ }
390
+ settings = idp_metadata_parser.parse_remote(
391
+ "https://example.com/auth/saml2/idp/metadata",
392
+ validate_cert,
393
+ options
394
+ )
395
+ ```
396
+
388
397
  ### Parsing Metadata into an Hash
389
398
 
390
399
  The `OneLogin::RubySaml::IdpMetadataParser` also provides the methods `#parse_to_hash` and `#parse_remote_to_hash`.
@@ -400,7 +409,7 @@ but it can be done as follows:
400
409
  * Validate the Signature, providing the cert.
401
410
  * Provide the XML to the parse method if the signature was validated
402
411
 
403
- ```
412
+ ```ruby
404
413
  require "xml_security"
405
414
  require "onelogin/ruby-saml/utils"
406
415
  require "onelogin/ruby-saml/idp_metadata_parser"
@@ -434,7 +443,7 @@ if valid
434
443
  else
435
444
  print "Metadata Signarture failed to be verified with the cert provided"
436
445
  end
437
-
446
+ ```
438
447
 
439
448
  ## Retrieving Attributes
440
449
 
@@ -671,7 +680,7 @@ signature validation process will fail at the Identity Provider.
671
680
  Ruby SAML supports EncryptedAssertion. The Identity Provider will encrypt the Assertion with the
672
681
  public cert of the Service Provider. The Service Provider will decrypt the EncryptedAssertion with its private key.
673
682
 
674
- You may enable EncryptedAssertion as follows. This will add `<md:KeyDescriptor use="encrytion">` to your
683
+ You may enable EncryptedAssertion as follows. This will add `<md:KeyDescriptor use="encryption">` to your
675
684
  SP Metadata XML, to be read by the IdP.
676
685
 
677
686
  ```ruby
@@ -708,6 +717,48 @@ validation fails. You may disable such exceptions using the `settings.security[:
708
717
  settings.security[:soft] = true # Do not raise error on failed signature/certificate validations
709
718
  ```
710
719
 
720
+ #### Advanced SP Certificate Usage & Key Rollover
721
+
722
+ Ruby SAML provides the `settings.sp_cert_multi` parameter to enable the following
723
+ advanced usage scenarios:
724
+ - Rotating SP certificates and private keys without disruption of service.
725
+ - Specifying separate SP certificates for signing and encryption.
726
+
727
+ The `sp_cert_multi` parameter replaces `certificate` and `private_key`
728
+ (you may not specify both pparameters at the same time.) `sp_cert_multi` has the following shape:
729
+
730
+ ```ruby
731
+ settings.sp_cert_multi = {
732
+ signing: [
733
+ { certificate: cert1, private_key: private_key1 },
734
+ { certificate: cert2, private_key: private_key2 }
735
+ ],
736
+ encryption: [
737
+ { certificate: cert1, private_key: private_key1 },
738
+ { certificate: cert3, private_key: private_key1 }
739
+ ],
740
+ }
741
+ ```
742
+
743
+ Certificate rotation is acheived by inserting new certificates at the bottom of each list,
744
+ and then removing the old certificates from the top of the list once your IdPs have migrated.
745
+ A common practice is for apps to publish the current SP metadata at a URL endpoint and have
746
+ the IdP regularly poll for updates.
747
+
748
+ Note the following:
749
+ - You may re-use the same certificate and/or private key in multiple places, including for both signing and encryption.
750
+ - The IdP should attempt to verify signatures with *all* `:signing` certificates,
751
+ and permit if *any one* succeeds. When signing, Ruby SAML will use the first SP certificate
752
+ in the `sp_cert_multi[:signing]` array. This will be the first active/non-expired certificate
753
+ in the array if `settings.security[:check_sp_cert_expiration]` is true.
754
+ - The IdP may encrypt with any of the SP certificates in the `sp_cert_multi[:encryption]`
755
+ array. When decrypting, Ruby SAML attempt to decrypt with each SP private key in
756
+ `sp_cert_multi[:encryption]` until the decryption is successful. This will skip private
757
+ keys for inactive/expired certificates if `:check_sp_cert_expiration` is true.
758
+ - If `:check_sp_cert_expiration` is true, the generated SP metadata XML will not include
759
+ inactive/expired certificates. This avoids validation errors when the IdP reads the SP
760
+ metadata.
761
+
711
762
  #### Audience Validation
712
763
 
713
764
  A service provider should only consider a SAML response valid if the IdP includes an <AudienceRestriction>
@@ -731,29 +782,6 @@ is invalid using the `settings.security[:strict_audience_validation]` parameter.
731
782
  settings.security[:strict_audience_validation] = true
732
783
  ```
733
784
 
734
- #### Key Rollover
735
-
736
- To update the SP X.509 certificate and private key without disruption of service, you may define the parameter
737
- `settings.certificate_new`. This will publish the new SP certificate in your metadata so that your IdP counterparties
738
- may cache it in preparation for rollover.
739
-
740
- For example, if you to rollover from `CERT A` to `CERT B`. Before rollover, your settings should look as follows.
741
- Both `CERT A` and `CERT B` will now appear in your SP metadata, however `CERT A` will still be used for signing
742
- and encryption at this time.
743
-
744
- ```ruby
745
- settings.certificate = "CERT A"
746
- settings.private_key = "PRIVATE KEY FOR CERT A"
747
- settings.certificate_new = "CERT B"
748
- ```
749
-
750
- After the IdP has cached `CERT B`, you may then change your settings as follows:
751
-
752
- ```ruby
753
- settings.certificate = "CERT B"
754
- settings.private_key = "PRIVATE KEY FOR CERT B"
755
- ```
756
-
757
785
  ## Single Log Out
758
786
 
759
787
  Ruby SAML supports SP-initiated Single Logout and IdP-Initiated Single Logout.
@@ -72,9 +72,10 @@ module OneLogin
72
72
  request = deflate(request) if settings.compress_request
73
73
  base64_request = encode(request)
74
74
  request_params = {"SAMLRequest" => base64_request}
75
+ sp_signing_key = settings.get_sp_signing_key
75
76
 
76
- if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && settings.private_key
77
- params['SigAlg'] = settings.security[:signature_method]
77
+ if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && sp_signing_key
78
+ params['SigAlg'] = settings.security[:signature_method]
78
79
  url_string = OneLogin::RubySaml::Utils.build_query(
79
80
  :type => 'SAMLRequest',
80
81
  :data => base64_request,
@@ -82,7 +83,7 @@ module OneLogin
82
83
  :sig_alg => params['SigAlg']
83
84
  )
84
85
  sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
85
- signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
86
+ signature = sp_signing_key.sign(sign_algorithm.new, url_string)
86
87
  params['Signature'] = encode(signature)
87
88
  end
88
89
 
@@ -179,15 +180,13 @@ module OneLogin
179
180
  end
180
181
 
181
182
  def sign_document(document, settings)
182
- if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && settings.private_key && settings.certificate
183
- private_key = settings.get_sp_key
184
- cert = settings.get_sp_cert
183
+ cert, private_key = settings.get_sp_signing_pair
184
+ if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && private_key && cert
185
185
  document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
186
186
  end
187
187
 
188
188
  document
189
189
  end
190
-
191
190
  end
192
191
  end
193
192
  end
@@ -186,8 +186,6 @@ module OneLogin
186
186
  idpsso_descriptors.map {|id| IdpMetadata.new(id, id.parent.attributes["entityID"])}
187
187
  end
188
188
 
189
- private
190
-
191
189
  # Retrieve the remote IdP metadata from the URL or a cached copy.
192
190
  # @param url [String] Url where the XML of the Identity Provider Metadata is published.
193
191
  # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
@@ -220,6 +218,8 @@ module OneLogin
220
218
  )
221
219
  end
222
220
 
221
+ private
222
+
223
223
  class IdpMetadata
224
224
  attr_reader :idpsso_descriptor, :entity_id
225
225
 
@@ -69,8 +69,9 @@ module OneLogin
69
69
  request = deflate(request) if settings.compress_request
70
70
  base64_request = encode(request)
71
71
  request_params = {"SAMLRequest" => base64_request}
72
+ sp_signing_key = settings.get_sp_signing_key
72
73
 
73
- if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && settings.private_key
74
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && sp_signing_key
74
75
  params['SigAlg'] = settings.security[:signature_method]
75
76
  url_string = OneLogin::RubySaml::Utils.build_query(
76
77
  :type => 'SAMLRequest',
@@ -79,7 +80,7 @@ module OneLogin
79
80
  :sig_alg => params['SigAlg']
80
81
  )
81
82
  sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
82
- signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
83
+ signature = settings.get_sp_signing_key.sign(sign_algorithm.new, url_string)
83
84
  params['Signature'] = encode(signature)
84
85
  end
85
86
 
@@ -138,9 +139,8 @@ module OneLogin
138
139
 
139
140
  def sign_document(document, settings)
140
141
  # embed signature
141
- if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && settings.private_key && settings.certificate
142
- private_key = settings.get_sp_key
143
- cert = settings.get_sp_cert
142
+ cert, private_key = settings.get_sp_signing_pair
143
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && private_key && cert
144
144
  document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
145
145
  end
146
146
 
@@ -62,29 +62,14 @@ module OneLogin
62
62
  }
63
63
  end
64
64
 
65
- # Add KeyDescriptor if messages will be signed / encrypted
66
- # with SP certificate, and new SP certificate if any
65
+ # Add KeyDescriptor elements for SP certificates.
67
66
  def add_sp_certificates(sp_sso, settings)
68
- cert = settings.get_sp_cert
69
- cert_new = settings.get_sp_cert_new
70
-
71
- for sp_cert in [cert, cert_new]
72
- if sp_cert
73
- cert_text = Base64.encode64(sp_cert.to_der).gsub("\n", '')
74
- kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
75
- ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
76
- xd = ki.add_element "ds:X509Data"
77
- xc = xd.add_element "ds:X509Certificate"
78
- xc.text = cert_text
79
-
80
- if settings.security[:want_assertions_encrypted]
81
- kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" }
82
- ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
83
- xd2 = ki2.add_element "ds:X509Data"
84
- xc2 = xd2.add_element "ds:X509Certificate"
85
- xc2.text = cert_text
86
- end
87
- end
67
+ certs = settings.get_sp_certs
68
+
69
+ certs[:signing].each { |cert, _| add_sp_cert_element(sp_sso, cert, :signing) }
70
+
71
+ if settings.security[:want_assertions_encrypted]
72
+ certs[:encryption].each { |cert, _| add_sp_cert_element(sp_sso, cert, :encryption) }
88
73
  end
89
74
 
90
75
  sp_sso
@@ -153,8 +138,7 @@ module OneLogin
153
138
  def embed_signature(meta_doc, settings)
154
139
  return unless settings.security[:metadata_signed]
155
140
 
156
- private_key = settings.get_sp_key
157
- cert = settings.get_sp_cert
141
+ cert, private_key = settings.get_sp_signing_pair
158
142
  return unless private_key && cert
159
143
 
160
144
  meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
@@ -172,6 +156,18 @@ module OneLogin
172
156
 
173
157
  ret
174
158
  end
159
+
160
+ private
161
+
162
+ def add_sp_cert_element(sp_sso, cert, use)
163
+ return unless cert
164
+ cert_text = Base64.encode64(cert.to_der).gsub("\n", '')
165
+ kd = sp_sso.add_element "md:KeyDescriptor", { "use" => use.to_s }
166
+ ki = kd.add_element "ds:KeyInfo", { "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#" }
167
+ xd = ki.add_element "ds:X509Data"
168
+ xc = xd.add_element "ds:X509Certificate"
169
+ xc.text = cert_text
170
+ end
175
171
  end
176
172
  end
177
173
  end
@@ -915,9 +915,9 @@ module OneLogin
915
915
  begin
916
916
  encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID')
917
917
  if encrypted_node
918
- node = decrypt_nameid(encrypted_node)
918
+ decrypt_nameid(encrypted_node)
919
919
  else
920
- node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
920
+ xpath_first_from_signed_assertion('/a:Subject/a:NameID')
921
921
  end
922
922
  end
923
923
  end
@@ -969,7 +969,7 @@ module OneLogin
969
969
  # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted
970
970
  #
971
971
  def generate_decrypted_document
972
- if settings.nil? || !settings.get_sp_key
972
+ if settings.nil? || settings.get_sp_decryption_keys.empty?
973
973
  raise ValidationError.new('An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method')
974
974
  end
975
975
 
@@ -1012,42 +1012,42 @@ module OneLogin
1012
1012
  end
1013
1013
 
1014
1014
  # Decrypts an EncryptedID element
1015
- # @param encryptedid_node [REXML::Element] The EncryptedID element
1015
+ # @param encrypted_id_node [REXML::Element] The EncryptedID element
1016
1016
  # @return [REXML::Document] The decrypted EncrypedtID element
1017
1017
  #
1018
- def decrypt_nameid(encryptedid_node)
1019
- decrypt_element(encryptedid_node, /(.*<\/(\w+:)?NameID>)/m)
1018
+ def decrypt_nameid(encrypted_id_node)
1019
+ decrypt_element(encrypted_id_node, /(.*<\/(\w+:)?NameID>)/m)
1020
1020
  end
1021
1021
 
1022
- # Decrypts an EncryptedID element
1023
- # @param encryptedid_node [REXML::Element] The EncryptedID element
1024
- # @return [REXML::Document] The decrypted EncrypedtID element
1022
+ # Decrypts an EncryptedAttribute element
1023
+ # @param encrypted_attribute_node [REXML::Element] The EncryptedAttribute element
1024
+ # @return [REXML::Document] The decrypted EncryptedAttribute element
1025
1025
  #
1026
- def decrypt_attribute(encryptedattribute_node)
1027
- decrypt_element(encryptedattribute_node, /(.*<\/(\w+:)?Attribute>)/m)
1026
+ def decrypt_attribute(encrypted_attribute_node)
1027
+ decrypt_element(encrypted_attribute_node, /(.*<\/(\w+:)?Attribute>)/m)
1028
1028
  end
1029
1029
 
1030
1030
  # Decrypt an element
1031
- # @param encryptedid_node [REXML::Element] The encrypted element
1032
- # @param rgrex string Regex
1031
+ # @param encrypt_node [REXML::Element] The encrypted element
1032
+ # @param regexp [Regexp] The regular expression to extract the decrypted data
1033
1033
  # @return [REXML::Document] The decrypted element
1034
1034
  #
1035
- def decrypt_element(encrypt_node, rgrex)
1036
- if settings.nil? || !settings.get_sp_key
1035
+ def decrypt_element(encrypt_node, regexp)
1036
+ if settings.nil? || settings.get_sp_decryption_keys.empty?
1037
1037
  raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it')
1038
1038
  end
1039
1039
 
1040
-
1041
1040
  if encrypt_node.name == 'EncryptedAttribute'
1042
1041
  node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
1043
1042
  else
1044
1043
  node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'
1045
1044
  end
1046
1045
 
1047
- elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key)
1046
+ elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypt_node, settings.get_sp_decryption_keys)
1047
+
1048
1048
  # If we get some problematic noise in the plaintext after decrypting.
1049
1049
  # This quick regexp parse will grab only the Element and discard the noise.
1050
- elem_plaintext = elem_plaintext.match(rgrex)[0]
1050
+ elem_plaintext = elem_plaintext.match(regexp)[0]
1051
1051
 
1052
1052
  # To avoid namespace errors if saml namespace is not defined
1053
1053
  # create a parent node first with the namespace defined
@@ -60,8 +60,8 @@ module OneLogin
60
60
  attr_accessor :attributes_index
61
61
  attr_accessor :force_authn
62
62
  attr_accessor :certificate
63
- attr_accessor :certificate_new
64
63
  attr_accessor :private_key
64
+ attr_accessor :sp_cert_multi
65
65
  attr_accessor :authn_context
66
66
  attr_accessor :authn_context_comparison
67
67
  attr_accessor :authn_context_decl_ref
@@ -70,6 +70,7 @@ module OneLogin
70
70
  attr_accessor :security
71
71
  attr_accessor :soft
72
72
  # Deprecated
73
+ attr_accessor :certificate_new
73
74
  attr_accessor :assertion_consumer_logout_service_url
74
75
  attr_reader :assertion_consumer_logout_service_binding
75
76
  attr_accessor :issuer
@@ -180,10 +181,7 @@ module OneLogin
180
181
  # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it)
181
182
  #
182
183
  def get_idp_cert
183
- return nil if idp_cert.nil? || idp_cert.empty?
184
-
185
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
186
- OpenSSL::X509::Certificate.new(formatted_cert)
184
+ OneLogin::RubySaml::Utils.build_cert_object(idp_cert)
187
185
  end
188
186
 
189
187
  # @return [Hash with 2 arrays of OpenSSL::X509::Certificate] Build multiple IdP certificates from the settings.
@@ -191,7 +189,7 @@ module OneLogin
191
189
  def get_idp_cert_multi
192
190
  return nil if idp_cert_multi.nil? || idp_cert_multi.empty?
193
191
 
194
- raise ArgumentError.new("Invalid value for idp_cert_multi") if not idp_cert_multi.is_a?(Hash)
192
+ raise ArgumentError.new("Invalid value for idp_cert_multi") unless idp_cert_multi.is_a?(Hash)
195
193
 
196
194
  certs = {:signing => [], :encryption => [] }
197
195
 
@@ -200,49 +198,70 @@ module OneLogin
200
198
  next if !certs_for_type || certs_for_type.empty?
201
199
 
202
200
  certs_for_type.each do |idp_cert|
203
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
204
- certs[type].push(OpenSSL::X509::Certificate.new(formatted_cert))
201
+ certs[type].push(OneLogin::RubySaml::Utils.build_cert_object(idp_cert))
205
202
  end
206
203
  end
207
204
 
208
205
  certs
209
206
  end
210
207
 
211
- # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
212
- #
213
- def get_sp_cert
214
- return nil if certificate.nil? || certificate.empty?
208
+ # @return [Hash<Symbol, Array<Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>>>]
209
+ # Build the SP certificates and private keys from the settings. If
210
+ # check_sp_cert_expiration is true, only returns certificates and private keys
211
+ # that are not expired.
212
+ def get_sp_certs
213
+ certs = get_all_sp_certs
214
+ return certs unless security[:check_sp_cert_expiration]
215
215
 
216
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
217
- cert = OpenSSL::X509::Certificate.new(formatted_cert)
216
+ active_certs = { signing: [], encryption: [] }
217
+ certs.each do |use, pairs|
218
+ next if pairs.empty?
218
219
 
219
- if security[:check_sp_cert_expiration]
220
- if OneLogin::RubySaml::Utils.is_cert_expired(cert)
221
- raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.")
222
- end
220
+ pairs = pairs.select { |cert, _| !cert || OneLogin::RubySaml::Utils.is_cert_active(cert) }
221
+ raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.") if pairs.empty?
222
+
223
+ active_certs[use] = pairs.freeze
223
224
  end
225
+ active_certs.freeze
226
+ end
224
227
 
225
- cert
228
+ # @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>]
229
+ # The SP signing certificate and private key.
230
+ def get_sp_signing_pair
231
+ get_sp_certs[:signing].first
226
232
  end
227
233
 
228
- # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it)
229
- #
230
- def get_sp_cert_new
231
- return nil if certificate_new.nil? || certificate_new.empty?
234
+ # @return [OpenSSL::X509::Certificate] The SP signing certificate.
235
+ # @deprecated Use get_sp_signing_pair or get_sp_certs instead.
236
+ def get_sp_cert
237
+ node = get_sp_signing_pair
238
+ node[0] if node
239
+ end
232
240
 
233
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new)
234
- OpenSSL::X509::Certificate.new(formatted_cert)
241
+ # @return [OpenSSL::PKey::RSA] The SP signing key.
242
+ def get_sp_signing_key
243
+ node = get_sp_signing_pair
244
+ node[1] if node
235
245
  end
236
246
 
237
- # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it)
238
- #
239
- def get_sp_key
240
- return nil if private_key.nil? || private_key.empty?
247
+ # @deprecated Use get_sp_signing_key or get_sp_certs instead.
248
+ alias_method :get_sp_key, :get_sp_signing_key
241
249
 
242
- formatted_private_key = OneLogin::RubySaml::Utils.format_private_key(private_key)
243
- OpenSSL::PKey::RSA.new(formatted_private_key)
250
+ # @return [Array<OpenSSL::PKey::RSA>] The SP decryption keys.
251
+ def get_sp_decryption_keys
252
+ ary = get_sp_certs[:encryption].map { |pair| pair[1] }
253
+ ary.compact!
254
+ ary.uniq!(&:to_pem)
255
+ ary.freeze
244
256
  end
245
257
 
258
+ # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings.
259
+ #
260
+ # @deprecated Use get_sp_certs instead
261
+ def get_sp_cert_new
262
+ node = get_sp_certs[:signing].last
263
+ node[0] if node
264
+ end
246
265
 
247
266
  def idp_binding_from_embed_sign
248
267
  security[:embed_sign] ? Utils::BINDINGS[:post] : Utils::BINDINGS[:redirect]
@@ -280,6 +299,85 @@ module OneLogin
280
299
  :lowercase_url_encoding => false
281
300
  }.freeze
282
301
  }.freeze
302
+
303
+ private
304
+
305
+ # @return [Hash<Symbol, Array<Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>>>]
306
+ # Build the SP certificates and private keys from the settings. Returns all
307
+ # certificates and private keys, even if they are expired.
308
+ def get_all_sp_certs
309
+ validate_sp_certs_params!
310
+ get_sp_certs_multi || get_sp_certs_single
311
+ end
312
+
313
+ # Validate certificate, certificate_new, private_key, and sp_cert_multi params.
314
+ def validate_sp_certs_params!
315
+ multi = sp_cert_multi && !sp_cert_multi.empty?
316
+ cert = certificate && !certificate.empty?
317
+ cert_new = certificate_new && !certificate_new.empty?
318
+ pk = private_key && !private_key.empty?
319
+ if multi && (cert || cert_new || pk)
320
+ raise ArgumentError.new("Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters")
321
+ end
322
+ end
323
+
324
+ # Get certs from certificate, certificate_new, and private_key parameters.
325
+ def get_sp_certs_single
326
+ certs = { :signing => [], :encryption => [] }
327
+
328
+ sp_key = OneLogin::RubySaml::Utils.build_private_key_object(private_key)
329
+ cert = OneLogin::RubySaml::Utils.build_cert_object(certificate)
330
+ if cert || sp_key
331
+ ary = [cert, sp_key].freeze
332
+ certs[:signing] << ary
333
+ certs[:encryption] << ary
334
+ end
335
+
336
+ cert_new = OneLogin::RubySaml::Utils.build_cert_object(certificate_new)
337
+ if cert_new
338
+ ary = [cert_new, sp_key].freeze
339
+ certs[:signing] << ary
340
+ certs[:encryption] << ary
341
+ end
342
+
343
+ certs
344
+ end
345
+
346
+ # Get certs from get_sp_cert_multi parameter.
347
+ def get_sp_certs_multi
348
+ return if sp_cert_multi.nil? || sp_cert_multi.empty?
349
+
350
+ raise ArgumentError.new("sp_cert_multi must be a Hash") unless sp_cert_multi.is_a?(Hash)
351
+
352
+ certs = { :signing => [], :encryption => [] }.freeze
353
+
354
+ [:signing, :encryption].each do |type|
355
+ certs_for_type = sp_cert_multi[type] || sp_cert_multi[type.to_s]
356
+ next if !certs_for_type || certs_for_type.empty?
357
+
358
+ unless certs_for_type.is_a?(Array) && certs_for_type.all? { |cert| cert.is_a?(Hash) }
359
+ raise ArgumentError.new("sp_cert_multi :#{type} node must be an Array of Hashes")
360
+ end
361
+
362
+ certs_for_type.each do |pair|
363
+ cert = pair[:certificate] || pair['certificate'] || pair[:cert] || pair['cert']
364
+ key = pair[:private_key] || pair['private_key'] || pair[:key] || pair['key']
365
+
366
+ unless cert && key
367
+ raise ArgumentError.new("sp_cert_multi :#{type} node Hashes must specify keys :certificate and :private_key")
368
+ end
369
+
370
+ certs[type] << [
371
+ OneLogin::RubySaml::Utils.build_cert_object(cert),
372
+ OneLogin::RubySaml::Utils.build_private_key_object(key)
373
+ ].freeze
374
+ end
375
+ end
376
+
377
+ certs.each { |_, ary| ary.freeze }
378
+ certs
379
+ end
283
380
  end
284
381
  end
285
382
  end
383
+
@@ -62,10 +62,7 @@ module OneLogin
62
62
  # @return [String] Gets the NameID of the Logout Request.
63
63
  #
64
64
  def name_id
65
- @name_id ||= begin
66
- node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
67
- Utils.element_text(node)
68
- end
65
+ @name_id ||= Utils.element_text(name_id_node)
69
66
  end
70
67
 
71
68
  alias_method :nameid, :name_id
@@ -73,15 +70,49 @@ module OneLogin
73
70
  # @return [String] Gets the NameID Format of the Logout Request.
74
71
  #
75
72
  def name_id_format
76
- @name_id_node ||= REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
77
73
  @name_id_format ||=
78
- if @name_id_node && @name_id_node.attribute("Format")
79
- @name_id_node.attribute("Format").value
74
+ if name_id_node && name_id_node.attribute("Format")
75
+ name_id_node.attribute("Format").value
80
76
  end
81
77
  end
82
78
 
83
79
  alias_method :nameid_format, :name_id_format
84
80
 
81
+ def name_id_node
82
+ @name_id_node ||=
83
+ begin
84
+ encrypted_node = REXML::XPath.first(document, "/p:LogoutRequest/a:EncryptedID", { "p" => PROTOCOL, "a" => ASSERTION })
85
+ if encrypted_node
86
+ node = decrypt_nameid(encrypted_node)
87
+ else
88
+ node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
89
+ end
90
+ end
91
+ end
92
+
93
+ # Decrypts an EncryptedID element
94
+ # @param encrypted_id_node [REXML::Element] The EncryptedID element
95
+ # @return [REXML::Document] The decrypted EncrypedtID element
96
+ #
97
+ def decrypt_nameid(encrypted_id_node)
98
+
99
+ if settings.nil? || settings.get_sp_decryption_keys.empty?
100
+ raise ValidationError.new('An ' + encrypted_id_node.name + ' found and no SP private key found on the settings to decrypt it')
101
+ end
102
+
103
+ elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypted_id_node, settings.get_sp_decryption_keys)
104
+ # If we get some problematic noise in the plaintext after decrypting.
105
+ # This quick regexp parse will grab only the Element and discard the noise.
106
+ elem_plaintext = elem_plaintext.match(/(.*<\/(\w+:)?NameID>)/m)[0]
107
+
108
+ # To avoid namespace errors if saml namespace is not defined
109
+ # create a parent node first with the namespace defined
110
+ node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'
111
+ elem_plaintext = node_header + elem_plaintext + '</node>'
112
+ doc = REXML::Document.new(elem_plaintext)
113
+ doc.root[0]
114
+ end
115
+
85
116
  # @return [String|nil] Gets the ID attribute from the Logout Request. if exists.
86
117
  #
87
118
  def id
@@ -78,9 +78,10 @@ module OneLogin
78
78
  response = deflate(response) if settings.compress_response
79
79
  base64_response = encode(response)
80
80
  response_params = {"SAMLResponse" => base64_response}
81
+ sp_signing_key = settings.get_sp_signing_key
81
82
 
82
- if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && settings.private_key
83
- params['SigAlg'] = settings.security[:signature_method]
83
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && sp_signing_key
84
+ params['SigAlg'] = settings.security[:signature_method]
84
85
  url_string = OneLogin::RubySaml::Utils.build_query(
85
86
  :type => 'SAMLResponse',
86
87
  :data => base64_response,
@@ -88,7 +89,7 @@ module OneLogin
88
89
  :sig_alg => params['SigAlg']
89
90
  )
90
91
  sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
91
- signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
92
+ signature = sp_signing_key.sign(sign_algorithm.new, url_string)
92
93
  params['Signature'] = encode(signature)
93
94
  end
94
95
 
@@ -150,9 +151,8 @@ module OneLogin
150
151
 
151
152
  def sign_document(document, settings)
152
153
  # embed signature
153
- if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.private_key && settings.certificate
154
- private_key = settings.get_sp_key
155
- cert = settings.get_sp_cert
154
+ cert, private_key = settings.get_sp_signing_pair
155
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && private_key && cert
156
156
  document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
157
157
  end
158
158
 
@@ -34,16 +34,24 @@ module OneLogin
34
34
  $)x.freeze
35
35
  UUID_PREFIX = '_'
36
36
 
37
- # Checks if the x509 cert provided is expired
38
- #
39
- # @param cert [Certificate] The x509 certificate
37
+ # Checks if the x509 cert provided is expired.
40
38
  #
39
+ # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate.
40
+ # @return [true|false] Whether the certificate is expired.
41
41
  def self.is_cert_expired(cert)
42
- if cert.is_a?(String)
43
- cert = OpenSSL::X509::Certificate.new(cert)
44
- end
42
+ cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String)
45
43
 
46
- return cert.not_after < Time.now
44
+ cert.not_after < Time.now
45
+ end
46
+
47
+ # Checks if the x509 cert provided has both started and has not expired.
48
+ #
49
+ # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate.
50
+ # @return [true|false] Whether the certificate is currently active.
51
+ def self.is_cert_active(cert)
52
+ cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String)
53
+ now = Time.now
54
+ cert.not_before <= now && cert.not_after >= now
47
55
  end
48
56
 
49
57
  # Interprets a ISO8601 duration value relative to a given timestamp.
@@ -61,20 +69,26 @@ module OneLogin
61
69
  matches = duration.match(DURATION_FORMAT)
62
70
 
63
71
  if matches.nil?
64
- raise Exception.new("Invalid ISO 8601 duration")
72
+ raise StandardError.new("Invalid ISO 8601 duration")
65
73
  end
66
74
 
67
75
  sign = matches[1] == '-' ? -1 : 1
68
76
 
69
77
  durYears, durMonths, durDays, durHours, durMinutes, durSeconds, durWeeks =
70
- matches[2..8].map { |match| match ? sign * match.tr(',', '.').to_f : 0.0 }
71
-
72
- initial_datetime = Time.at(timestamp).utc.to_datetime
73
- final_datetime = initial_datetime.next_year(durYears)
74
- final_datetime = final_datetime.next_month(durMonths)
75
- final_datetime = final_datetime.next_day((7*durWeeks) + durDays)
76
- final_timestamp = final_datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
77
- return final_timestamp
78
+ matches[2..8].map do |match|
79
+ if match
80
+ match = match.tr(',', '.').gsub(/\.0*\z/, '')
81
+ sign * (match.include?('.') ? match.to_f : match.to_i)
82
+ else
83
+ 0
84
+ end
85
+ end
86
+
87
+ datetime = Time.at(timestamp).utc.to_datetime
88
+ datetime = datetime.next_year(durYears)
89
+ datetime = datetime.next_month(durMonths)
90
+ datetime = datetime.next_day((7*durWeeks) + durDays)
91
+ datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
78
92
  end
79
93
 
80
94
  # Return a properly formatted x509 certificate
@@ -128,6 +142,28 @@ module OneLogin
128
142
  "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
129
143
  end
130
144
 
145
+ # Given a certificate string, return an OpenSSL::X509::Certificate object.
146
+ #
147
+ # @param cert [String] The original certificate
148
+ # @return [OpenSSL::X509::Certificate] The certificate object
149
+ #
150
+ def self.build_cert_object(cert)
151
+ return nil if cert.nil? || cert.empty?
152
+
153
+ OpenSSL::X509::Certificate.new(format_cert(cert))
154
+ end
155
+
156
+ # Given a private key string, return an OpenSSL::PKey::RSA object.
157
+ #
158
+ # @param cert [String] The original private key
159
+ # @return [OpenSSL::PKey::RSA] The private key object
160
+ #
161
+ def self.build_private_key_object(private_key)
162
+ return nil if private_key.nil? || private_key.empty?
163
+
164
+ OpenSSL::PKey::RSA.new(format_private_key(private_key))
165
+ end
166
+
131
167
  # Build the Query String signature that will be used in the HTTP-Redirect binding
132
168
  # to generate the Signature
133
169
  # @param params [Hash] Parameters to build the Query String
@@ -199,7 +235,7 @@ module OneLogin
199
235
 
200
236
  # Validate the Signature parameter sent on the HTTP-Redirect binding
201
237
  # @param params [Hash] Parameters to be used in the validation process
202
- # @option params [OpenSSL::X509::Certificate] cert The Identity provider public certtificate
238
+ # @option params [OpenSSL::X509::Certificate] cert The IDP public certificate
203
239
  # @option params [String] sig_alg The SigAlg parameter
204
240
  # @option params [String] signature The Signature parameter (base64 encoded)
205
241
  # @option params [String] query_string The full GET Query String to be compared
@@ -236,9 +272,29 @@ module OneLogin
236
272
  error_msg
237
273
  end
238
274
 
275
+ # Obtains the decrypted string from an Encrypted node element in XML,
276
+ # given multiple private keys to try.
277
+ # @param encrypted_node [REXML::Element] The Encrypted element
278
+ # @param private_keys [Array<OpenSSL::PKey::RSA>] The Service provider private key
279
+ # @return [String] The decrypted data
280
+ def self.decrypt_multi(encrypted_node, private_keys)
281
+ raise ArgumentError.new('private_keys must be specified') if !private_keys || private_keys.empty?
282
+
283
+ error = nil
284
+ private_keys.each do |key|
285
+ begin
286
+ return decrypt_data(encrypted_node, key)
287
+ rescue OpenSSL::PKey::PKeyError => e
288
+ error ||= e
289
+ end
290
+ end
291
+
292
+ raise(error) if error
293
+ end
294
+
239
295
  # Obtains the decrypted string from an Encrypted node element in XML
240
- # @param encrypted_node [REXML::Element] The Encrypted element
241
- # @param private_key [OpenSSL::PKey::RSA] The Service provider private key
296
+ # @param encrypted_node [REXML::Element] The Encrypted element
297
+ # @param private_key [OpenSSL::PKey::RSA] The Service provider private key
242
298
  # @return [String] The decrypted data
243
299
  def self.decrypt_data(encrypted_node, private_key)
244
300
  encrypt_data = REXML::XPath.first(
@@ -302,7 +358,7 @@ module OneLogin
302
358
 
303
359
  # Obtains the deciphered text
304
360
  # @param cipher_text [String] The ciphered text
305
- # @param symmetric_key [String] The symetric key used to encrypt the text
361
+ # @param symmetric_key [String] The symmetric key used to encrypt the text
306
362
  # @param algorithm [String] The encrypted algorithm
307
363
  # @return [String] The deciphered text
308
364
  def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm)
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '1.15.0'
3
+ VERSION = '1.17.0'
4
4
  end
5
5
  end
data/lib/xml_security.rb CHANGED
@@ -310,17 +310,30 @@ module XMLSecurity
310
310
  canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
311
311
  noko_sig_element.remove
312
312
 
313
+ # get signed info
314
+ signed_info_element = REXML::XPath.first(
315
+ sig_element,
316
+ "./ds:SignedInfo",
317
+ { "ds" => DSIG }
318
+ )
319
+
313
320
  # get inclusive namespaces
314
321
  inclusive_namespaces = extract_inclusive_namespaces
315
322
 
316
323
  # check digests
317
- ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
324
+ ref = REXML::XPath.first(signed_info_element, "./ds:Reference", {"ds"=>DSIG})
318
325
 
319
- hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
326
+ reference_nodes = document.xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
327
+
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
331
+
332
+ hashed_element = reference_nodes[0]
320
333
 
321
334
  canon_algorithm = canon_algorithm REXML::XPath.first(
322
- ref,
323
- '//ds:CanonicalizationMethod',
335
+ signed_info_element,
336
+ './ds:CanonicalizationMethod',
324
337
  { "ds" => DSIG }
325
338
  )
326
339
 
@@ -330,13 +343,13 @@ module XMLSecurity
330
343
 
331
344
  digest_algorithm = algorithm(REXML::XPath.first(
332
345
  ref,
333
- "//ds:DigestMethod",
346
+ "./ds:DigestMethod",
334
347
  { "ds" => DSIG }
335
348
  ))
336
349
  hash = digest_algorithm.digest(canon_hashed_element)
337
350
  encoded_digest_value = REXML::XPath.first(
338
351
  ref,
339
- "//ds:DigestValue",
352
+ "./ds:DigestValue",
340
353
  { "ds" => DSIG }
341
354
  )
342
355
  digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
@@ -362,7 +375,7 @@ module XMLSecurity
362
375
  def process_transforms(ref, canon_algorithm)
363
376
  transforms = REXML::XPath.match(
364
377
  ref,
365
- "//ds:Transforms/ds:Transform",
378
+ "./ds:Transforms/ds:Transform",
366
379
  { "ds" => DSIG }
367
380
  )
368
381
 
data/ruby-saml.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
16
16
  "README.md"
17
17
  ]
18
18
  s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
- s.homepage = %q{https://github.com/saml-toolkit/ruby-saml}
19
+ s.homepage = %q{https://github.com/saml-toolkits/ruby-saml}
20
20
  s.rdoc_options = ["--charset=UTF-8"]
21
21
  s.require_paths = ["lib"]
22
22
  s.rubygems_version = %q{1.3.7}
@@ -66,7 +66,7 @@ Gem::Specification.new do |s|
66
66
  s.add_development_dependency('simplecov-lcov', '>0.7.0')
67
67
  end
68
68
 
69
- s.add_development_dependency('minitest', '~> 5.5')
69
+ s.add_development_dependency('minitest', '~> 5.5', '<5.19.0')
70
70
  s.add_development_dependency('mocha', '~> 0.14')
71
71
 
72
72
  if RUBY_VERSION < '2.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.15.0
4
+ version: 1.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SAML Toolkit
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-01-04 00:00:00.000000000 Z
12
+ date: 2024-09-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
@@ -74,6 +74,9 @@ dependencies:
74
74
  - - "~>"
75
75
  - !ruby/object:Gem::Version
76
76
  version: '5.5'
77
+ - - "<"
78
+ - !ruby/object:Gem::Version
79
+ version: 5.19.0
77
80
  type: :development
78
81
  prerelease: false
79
82
  version_requirements: !ruby/object:Gem::Requirement
@@ -81,6 +84,9 @@ dependencies:
81
84
  - - "~>"
82
85
  - !ruby/object:Gem::Version
83
86
  version: '5.5'
87
+ - - "<"
88
+ - !ruby/object:Gem::Version
89
+ version: 5.19.0
84
90
  - !ruby/object:Gem::Dependency
85
91
  name: mocha
86
92
  requirement: !ruby/object:Gem::Requirement
@@ -177,6 +183,7 @@ extra_rdoc_files:
177
183
  - README.md
178
184
  files:
179
185
  - ".document"
186
+ - ".github/FUNDING.yml"
180
187
  - ".github/workflows/test.yml"
181
188
  - ".gitignore"
182
189
  - CHANGELOG.md
@@ -221,7 +228,7 @@ files:
221
228
  - lib/schemas/xmldsig-core-schema.xsd
222
229
  - lib/xml_security.rb
223
230
  - ruby-saml.gemspec
224
- homepage: https://github.com/saml-toolkit/ruby-saml
231
+ homepage: https://github.com/saml-toolkits/ruby-saml
225
232
  licenses:
226
233
  - MIT
227
234
  metadata: {}
@@ -241,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
241
248
  - !ruby/object:Gem::Version
242
249
  version: '0'
243
250
  requirements: []
244
- rubygems_version: 3.3.26
251
+ rubygems_version: 3.5.18
245
252
  signing_key:
246
253
  specification_version: 4
247
254
  summary: SAML Ruby Tookit