Skip to content

Commit f7b355b

Browse files
committed
Merge remote-tracking branch 'remotes/origin/master' into v2.x
2 parents d159c46 + f40c59b commit f7b355b

24 files changed

+1206
-161
lines changed

.github/FUNDING.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# These are supported funding model platforms
2+
3+
github: [SAML-Toolkits]

CHANGELOG.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
# Ruby SAML Changelog
22

3+
### 1.17.0
4+
* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Add `Settings#sp_cert_multi` paramter to facilitate SP certificate and key rotation.
5+
* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Support multiple simultaneous SP decryption keys via `Settings#sp_cert_multi` parameter.
6+
* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Deprecate `Settings#certificate_new` parameter.
7+
* [#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.
8+
* [#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`.
9+
* [#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.
10+
11+
### 1.16.0 (Oct 09, 2023)
12+
* [#671](https://github.com/SAML-Toolkits/ruby-saml/pull/671) Add support on LogoutRequest with Encrypted NameID
13+
314
### 1.15.0 (Jan 04, 2023)
415
* [#650](https://github.com/SAML-Toolkits/ruby-saml/pull/650) Replace strip! by strip on compute_digest method
516
* [#638](https://github.com/SAML-Toolkits/ruby-saml/pull/638) Fix dateTime format for the validUntil attribute of the generated metadata
6-
* [#576](https://github.com/SAML-Toolkits/ruby-saml/pull/576) Support idp cert multi with string keys
17+
* [#576](https://github.com/SAML-Toolkits/ruby-saml/pull/576) Support `Settings#idp_cert_multi` with string keys
718
* [#567](https://github.com/SAML-Toolkits/ruby-saml/pull/567) Improve Code quality
819
* Add info about new repo, new maintainer, new security contact
920
* Fix tests, Adjust dependencies, Add ruby 3.2 and new jruby versions tests to the CI. Add coveralls support
@@ -48,7 +59,7 @@
4859
* Support Process Transform
4960
* Raise SettingError if invoking an action with no endpoint defined on the settings
5061
* Made IdpMetadataParser more extensible for subclasses
51-
*[#548](https://github.com/onelogin/ruby-saml/pull/548) Add :skip_audience option
62+
* [#548](https://github.com/onelogin/ruby-saml/pull/548) Add :skip_audience option
5263
* [#555](https://github.com/onelogin/ruby-saml/pull/555) Define 'soft' variable to prevent exception when doc cert is invalid
5364
* Improve documentation
5465

@@ -107,7 +118,6 @@
107118
* [#423](https://github.com/onelogin/ruby-saml/pull/423) Allow format_cert to work with chained certificates
108119
* [#422](https://github.com/onelogin/ruby-saml/pull/422) Use to_s for requested attribute value
109120

110-
111121
### 1.5.0 (August 31, 2017)
112122
* [#400](https://github.com/onelogin/ruby-saml/pull/400) When validating Signature use stored IdP certficate if Signature contains no info about Certificate
113123
* [#402](https://github.com/onelogin/ruby-saml/pull/402) Fix validate_response_state method that rejected SAMLResponses when using idp_cert_multi and idp_cert and idp_cert_fingerprint were not provided.
@@ -116,7 +126,6 @@
116126
* [#374](https://github.com/onelogin/ruby-saml/issues/374) Support more than one level of StatusCode
117127
* [#405](https://github.com/onelogin/ruby-saml/pull/405) Support ADFS encrypted key (Accept KeyInfo nodes with no ds namespace)
118128

119-
120129
### 1.4.3 (May 18, 2017)
121130
* Added SubjectConfirmation Recipient validation
122131
* [#393](https://github.com/onelogin/ruby-saml/pull/393) Implement IdpMetadataParser#parse_to_hash

README.md

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,7 @@ signature validation process will fail at the Identity Provider.
683683
Ruby SAML supports EncryptedAssertion. The Identity Provider will encrypt the Assertion with the
684684
public cert of the Service Provider. The Service Provider will decrypt the EncryptedAssertion with its private key.
685685
686-
You may enable EncryptedAssertion as follows. This will add `<md:KeyDescriptor use="encrytion">` to your
686+
You may enable EncryptedAssertion as follows. This will add `<md:KeyDescriptor use="encryption">` to your
687687
SP Metadata XML, to be read by the IdP.
688688
689689
```ruby
@@ -720,6 +720,48 @@ validation fails. You may disable such exceptions using the `settings.security[:
720720
settings.security[:soft] = true # Do not raise error on failed signature/certificate validations
721721
```
722722
723+
#### Advanced SP Certificate Usage & Key Rollover
724+
725+
Ruby SAML provides the `settings.sp_cert_multi` parameter to enable the following
726+
advanced usage scenarios:
727+
- Rotating SP certificates and private keys without disruption of service.
728+
- Specifying separate SP certificates for signing and encryption.
729+
730+
The `sp_cert_multi` parameter replaces `certificate` and `private_key`
731+
(you may not specify both pparameters at the same time.) `sp_cert_multi` has the following shape:
732+
733+
```ruby
734+
settings.sp_cert_multi = {
735+
signing: [
736+
{ certificate: cert1, private_key: private_key1 },
737+
{ certificate: cert2, private_key: private_key2 }
738+
],
739+
encryption: [
740+
{ certificate: cert1, private_key: private_key1 },
741+
{ certificate: cert3, private_key: private_key1 }
742+
],
743+
}
744+
```
745+
746+
Certificate rotation is acheived by inserting new certificates at the bottom of each list,
747+
and then removing the old certificates from the top of the list once your IdPs have migrated.
748+
A common practice is for apps to publish the current SP metadata at a URL endpoint and have
749+
the IdP regularly poll for updates.
750+
751+
Note the following:
752+
- You may re-use the same certificate and/or private key in multiple places, including for both signing and encryption.
753+
- The IdP should attempt to verify signatures with *all* `:signing` certificates,
754+
and permit if *any one* succeeds. When signing, Ruby SAML will use the first SP certificate
755+
in the `sp_cert_multi[:signing]` array. This will be the first active/non-expired certificate
756+
in the array if `settings.security[:check_sp_cert_expiration]` is true.
757+
- The IdP may encrypt with any of the SP certificates in the `sp_cert_multi[:encryption]`
758+
array. When decrypting, Ruby SAML attempt to decrypt with each SP private key in
759+
`sp_cert_multi[:encryption]` until the decryption is successful. This will skip private
760+
keys for inactive/expired certificates if `:check_sp_cert_expiration` is true.
761+
- If `:check_sp_cert_expiration` is true, the generated SP metadata XML will not include
762+
inactive/expired certificates. This avoids validation errors when the IdP reads the SP
763+
metadata.
764+
723765
#### Audience Validation
724766
725767
A service provider should only consider a SAML response valid if the IdP includes an <AudienceRestriction>
@@ -743,29 +785,6 @@ is invalid using the `settings.security[:strict_audience_validation]` parameter.
743785
settings.security[:strict_audience_validation] = true
744786
```
745787
746-
#### Key Rollover
747-
748-
To update the SP X.509 certificate and private key without disruption of service, you may define the parameter
749-
`settings.certificate_new`. This will publish the new SP certificate in your metadata so that your IdP counterparties
750-
may cache it in preparation for rollover.
751-
752-
For example, if you to rollover from `CERT A` to `CERT B`. Before rollover, your settings should look as follows.
753-
Both `CERT A` and `CERT B` will now appear in your SP metadata, however `CERT A` will still be used for signing
754-
and encryption at this time.
755-
756-
```ruby
757-
settings.certificate = "CERT A"
758-
settings.private_key = "PRIVATE KEY FOR CERT A"
759-
settings.certificate_new = "CERT B"
760-
```
761-
762-
After the IdP has cached `CERT B`, you may then change your settings as follows:
763-
764-
```ruby
765-
settings.certificate = "CERT B"
766-
settings.private_key = "PRIVATE KEY FOR CERT B"
767-
```
768-
769788
## Single Log Out
770789
771790
Ruby SAML supports SP-initiated Single Logout and IdP-Initiated Single Logout.

lib/onelogin/ruby-saml/authrequest.rb

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,18 @@ def create_params(settings, params={})
7575
request = deflate(request) if settings.compress_request
7676
base64_request = encode(request)
7777
request_params = {"SAMLRequest" => base64_request}
78+
sp_signing_key = settings.get_sp_signing_key
7879

79-
if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && settings.private_key
80-
params['SigAlg'] = settings.security[:signature_method]
80+
if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && sp_signing_key
81+
params['SigAlg'] = settings.security[:signature_method]
8182
url_string = OneLogin::RubySaml::Utils.build_query(
8283
type: 'SAMLRequest',
8384
data: base64_request,
8485
relay_state: relay_state,
8586
sig_alg: params['SigAlg']
8687
)
8788
sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
88-
signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
89+
signature = sp_signing_key.sign(sign_algorithm.new, url_string)
8990
params['Signature'] = encode(signature)
9091
end
9192

@@ -183,15 +184,13 @@ def create_xml_document(settings)
183184
end
184185

185186
def sign_document(document, settings)
186-
if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && settings.private_key && settings.certificate
187-
private_key = settings.get_sp_key
188-
cert = settings.get_sp_cert
187+
cert, private_key = settings.get_sp_signing_pair
188+
if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && private_key && cert
189189
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
190190
end
191191

192192
document
193193
end
194-
195194
end
196195
end
197196
end

lib/onelogin/ruby-saml/logoutrequest.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ def create_params(settings, params={})
7272
request = deflate(request) if settings.compress_request
7373
base64_request = encode(request)
7474
request_params = {"SAMLRequest" => base64_request}
75+
sp_signing_key = settings.get_sp_signing_key
7576

76-
if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && settings.private_key
77+
if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && sp_signing_key
7778
params['SigAlg'] = settings.security[:signature_method]
7879
url_string = OneLogin::RubySaml::Utils.build_query(
7980
type: 'SAMLRequest',
@@ -82,7 +83,7 @@ def create_params(settings, params={})
8283
sig_alg: params['SigAlg']
8384
)
8485
sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
85-
signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
86+
signature = settings.get_sp_signing_key.sign(sign_algorithm.new, url_string)
8687
params['Signature'] = encode(signature)
8788
end
8889

@@ -141,9 +142,8 @@ def create_xml_document(settings)
141142

142143
def sign_document(document, settings)
143144
# embed signature
144-
if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && settings.private_key && settings.certificate
145-
private_key = settings.get_sp_key
146-
cert = settings.get_sp_cert
145+
cert, private_key = settings.get_sp_signing_pair
146+
if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && private_key && cert
147147
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
148148
end
149149

lib/onelogin/ruby-saml/metadata.rb

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,29 +64,14 @@ def add_sp_sso_element(root, settings)
6464
}
6565
end
6666

67-
# Add KeyDescriptor if messages will be signed / encrypted
68-
# with SP certificate, and new SP certificate if any
67+
# Add KeyDescriptor elements for SP certificates.
6968
def add_sp_certificates(sp_sso, settings)
70-
cert = settings.get_sp_cert
71-
cert_new = settings.get_sp_cert_new
72-
73-
[cert, cert_new].each do |sp_cert|
74-
next unless sp_cert
75-
76-
cert_text = Base64.encode64(sp_cert.to_der).gsub("\n", '')
77-
kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
78-
ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
79-
xd = ki.add_element "ds:X509Data"
80-
xc = xd.add_element "ds:X509Certificate"
81-
xc.text = cert_text
82-
83-
next unless settings.security[:want_assertions_encrypted]
84-
85-
kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" }
86-
ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
87-
xd2 = ki2.add_element "ds:X509Data"
88-
xc2 = xd2.add_element "ds:X509Certificate"
89-
xc2.text = cert_text
69+
certs = settings.get_sp_certs
70+
71+
certs[:signing].each { |cert, _| add_sp_cert_element(sp_sso, cert, :signing) }
72+
73+
if settings.security[:want_assertions_encrypted]
74+
certs[:encryption].each { |cert, _| add_sp_cert_element(sp_sso, cert, :encryption) }
9075
end
9176

9277
sp_sso
@@ -155,8 +140,7 @@ def add_extras(root, _settings)
155140
def embed_signature(meta_doc, settings)
156141
return unless settings.security[:metadata_signed]
157142

158-
private_key = settings.get_sp_key
159-
cert = settings.get_sp_cert
143+
cert, private_key = settings.get_sp_signing_pair
160144
return unless private_key && cert
161145

162146
meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
@@ -174,6 +158,18 @@ def output_xml(meta_doc, pretty_print)
174158

175159
ret
176160
end
161+
162+
private
163+
164+
def add_sp_cert_element(sp_sso, cert, use)
165+
return unless cert
166+
cert_text = Base64.encode64(cert.to_der).gsub("\n", '')
167+
kd = sp_sso.add_element "md:KeyDescriptor", { "use" => use.to_s }
168+
ki = kd.add_element "ds:KeyInfo", { "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#" }
169+
xd = ki.add_element "ds:X509Data"
170+
xc = xd.add_element "ds:X509Certificate"
171+
xc.text = cert_text
172+
end
177173
end
178174
end
179175
end

lib/onelogin/ruby-saml/response.rb

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -901,9 +901,9 @@ def name_id_node
901901
begin
902902
encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID')
903903
if encrypted_node
904-
node = decrypt_nameid(encrypted_node)
904+
decrypt_nameid(encrypted_node)
905905
else
906-
node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
906+
xpath_first_from_signed_assertion('/a:Subject/a:NameID')
907907
end
908908
end
909909
end
@@ -955,7 +955,7 @@ def xpath_from_signed_assertion(subelt=nil)
955955
# @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted
956956
#
957957
def generate_decrypted_document
958-
if settings.nil? || !settings.get_sp_key
958+
if settings.nil? || settings.get_sp_decryption_keys.empty?
959959
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')
960960
end
961961

@@ -993,28 +993,28 @@ def decrypt_assertion(encrypted_assertion_node)
993993
end
994994

995995
# Decrypts an EncryptedID element
996-
# @param encryptedid_node [REXML::Element] The EncryptedID element
996+
# @param encrypted_id_node [REXML::Element] The EncryptedID element
997997
# @return [REXML::Document] The decrypted EncrypedtID element
998998
#
999-
def decrypt_nameid(encryptedid_node)
1000-
decrypt_element(encryptedid_node, %r{(.*</(\w+:)?NameID>)}m)
999+
def decrypt_nameid(encrypted_id_node)
1000+
decrypt_element(encrypted_id_node, /(.*<\/(\w+:)?NameID>)/m)
10011001
end
10021002

1003-
# Decrypts an EncryptedID element
1004-
# @param encryptedid_node [REXML::Element] The EncryptedID element
1005-
# @return [REXML::Document] The decrypted EncrypedtID element
1003+
# Decrypts an EncryptedAttribute element
1004+
# @param encrypted_attribute_node [REXML::Element] The EncryptedAttribute element
1005+
# @return [REXML::Document] The decrypted EncryptedAttribute element
10061006
#
1007-
def decrypt_attribute(encryptedattribute_node)
1008-
decrypt_element(encryptedattribute_node, %r{(.*</(\w+:)?Attribute>)}m)
1007+
def decrypt_attribute(encrypted_attribute_node)
1008+
decrypt_element(encrypted_attribute_node, /(.*<\/(\w+:)?Attribute>)/m)
10091009
end
10101010

10111011
# Decrypt an element
1012-
# @param encryptedid_node [REXML::Element] The encrypted element
1013-
# @param rgrex string Regex
1012+
# @param encrypt_node [REXML::Element] The encrypted element
1013+
# @param regexp [Regexp] The regular expression to extract the decrypted data
10141014
# @return [REXML::Document] The decrypted element
10151015
#
1016-
def decrypt_element(encrypt_node, rgrex)
1017-
if settings.nil? || !settings.get_sp_key
1016+
def decrypt_element(encrypt_node, regexp)
1017+
if settings.nil? || settings.get_sp_decryption_keys.empty?
10181018
raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it')
10191019
end
10201020

@@ -1024,10 +1024,11 @@ def decrypt_element(encrypt_node, rgrex)
10241024
node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'
10251025
end
10261026

1027-
elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key)
1027+
elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypt_node, settings.get_sp_decryption_keys)
1028+
10281029
# If we get some problematic noise in the plaintext after decrypting.
10291030
# This quick regexp parse will grab only the Element and discard the noise.
1030-
elem_plaintext = elem_plaintext.match(rgrex)[0]
1031+
elem_plaintext = elem_plaintext.match(regexp)[0]
10311032

10321033
# To avoid namespace errors if saml namespace is not defined
10331034
# create a parent node first with the namespace defined

0 commit comments

Comments
 (0)