Skip to content

Commit 7394795

Browse files
committed
Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption.
1 parent e4621b6 commit 7394795

File tree

10 files changed

+386
-47
lines changed

10 files changed

+386
-47
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,31 @@ class SamlController < ApplicationController
251251
end
252252
end
253253
```
254+
255+
256+
## Signature validation
257+
258+
On the ruby-saml toolkit there are different ways to validate the signature of the SAMLResponse:
259+
- You can provide the IdP x509 public certificate at the 'idp_cert' setting.
260+
- You can provide the IdP x509 public certificate in fingerprint format using the 'idp_cert_fingerprint' setting parameter and additionally the 'idp_cert_fingerprint_algorithm' parameter.
261+
262+
When validating the signature of redirect binding, the fingerprint is useless and the the certficate of the IdP is required in order to execute the validation.
263+
You can pass the option :relax_signature_validation to SloLogoutrequest and Logoutresponse if want to avoid signature validation if no certificate of the IdP is provided.
264+
265+
In some scenarios the IdP uses different certificates for signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata.
266+
267+
In order to handle that the toolkit offers the 'idp_cert_multi' parameter.
268+
When used, 'idp_cert' and 'idp_cert_fingerprint' values are ignored.
269+
270+
That 'idp_cert_multi' must be a Hash as follows:
271+
{
272+
:signing => [],
273+
:encryption => []
274+
}
275+
276+
And on 'signing' and 'encryption' arrays, add the different IdP x509 public certificates published on the IdP metadata.
277+
278+
254279
## Metadata Based Configuration
255280
256281
The method above requires a little extra work to manually specify attributes about the IdP. (And your SP application) There's an easier method -- use a metadata exchange. Metadata is just an XML file that defines the capabilities of both the IdP and the SP application. It also contains the X.509 public

lib/onelogin/ruby-saml/logoutresponse.rb

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class Logoutresponse < SamlMessage
2727
# @param options [Hash] Extra parameters.
2828
# :matches_request_id It will validate that the logout response matches the ID of the request.
2929
# :get_params GET Parameters, including the SAMLResponse
30+
# :relax_signature_validation to accept signatures if no idp certificate registered on settings
31+
#
3032
# @raise [ArgumentError] if response is nil
3133
#
3234
def initialize(response, settings = nil, options = {})
@@ -208,21 +210,42 @@ def validate_signature
208210
return true unless !options.nil?
209211
return true unless options.has_key? :get_params
210212
return true unless options[:get_params].has_key? 'Signature'
211-
return true if settings.nil? || settings.get_idp_cert.nil?
212-
213+
214+
idp_cert = settings.get_idp_cert
215+
idp_certs = settings.get_idp_cert_multi
216+
217+
if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?)
218+
return options.has_key? :relax_signature_validation
219+
end
220+
213221
query_string = OneLogin::RubySaml::Utils.build_query(
214222
:type => 'SAMLResponse',
215223
:data => options[:get_params]['SAMLResponse'],
216224
:relay_state => options[:get_params]['RelayState'],
217225
:sig_alg => options[:get_params]['SigAlg']
218226
)
219227

220-
valid = OneLogin::RubySaml::Utils.verify_signature(
221-
:cert => settings.get_idp_cert,
222-
:sig_alg => options[:get_params]['SigAlg'],
223-
:signature => options[:get_params]['Signature'],
224-
:query_string => query_string
225-
)
228+
if idp_certs.nil? || idp_certs[:signing].empty?
229+
valid = OneLogin::RubySaml::Utils.verify_signature(
230+
:cert => settings.get_idp_cert,
231+
:sig_alg => options[:get_params]['SigAlg'],
232+
:signature => options[:get_params]['Signature'],
233+
:query_string => query_string
234+
)
235+
else
236+
valid = false
237+
idp_certs[:signing].each do |idp_cert|
238+
valid = OneLogin::RubySaml::Utils.verify_signature(
239+
:cert => idp_cert,
240+
:sig_alg => options[:get_params]['SigAlg'],
241+
:signature => options[:get_params]['Signature'],
242+
:query_string => query_string
243+
)
244+
if valid
245+
break
246+
end
247+
end
248+
end
226249

227250
unless valid
228251
error_msg = "Invalid Signature on Logout Response"

lib/onelogin/ruby-saml/response.rb

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -803,13 +803,27 @@ def validate_signature
803803
return append_error(error_msg)
804804
end
805805

806-
opts = {}
807-
opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm
808-
opts[:cert] = settings.get_idp_cert
809-
fingerprint = settings.get_fingerprint
810-
811-
unless fingerprint && doc.validate_document(fingerprint, @soft, opts)
812-
return append_error(error_msg)
806+
idp_certs = settings.get_idp_cert_multi
807+
if idp_certs.nil? || idp_certs[:signing].empty?
808+
opts = {}
809+
opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm
810+
opts[:cert] = settings.get_idp_cert
811+
fingerprint = settings.get_fingerprint
812+
813+
unless fingerprint && doc.validate_document(fingerprint, @soft, opts)
814+
return append_error(error_msg)
815+
end
816+
else
817+
valid = false
818+
idp_certs[:signing].each do |idp_cert|
819+
valid = doc.validate_document_with_cert(idp_cert)
820+
if valid
821+
break
822+
end
823+
end
824+
unless valid
825+
return append_error(error_msg)
826+
end
813827
end
814828

815829
true

lib/onelogin/ruby-saml/settings.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def initialize(overrides = {})
2828
attr_accessor :idp_cert
2929
attr_accessor :idp_cert_fingerprint
3030
attr_accessor :idp_cert_fingerprint_algorithm
31+
attr_accessor :idp_cert_multi
3132
attr_accessor :idp_attribute_names
3233
attr_accessor :idp_name_qualifier
3334
# SP Data
@@ -125,6 +126,32 @@ def get_idp_cert
125126
OpenSSL::X509::Certificate.new(formatted_cert)
126127
end
127128

129+
# @return [Hash with 2 arrays of OpenSSL::X509::Certificate] Build multiple IdP certificates from the settings.
130+
#
131+
def get_idp_cert_multi
132+
return nil if idp_cert_multi.nil? || idp_cert_multi.empty?
133+
134+
raise ArgumentError.new("Invalid value for idp_cert_multi") if not idp_cert_multi.is_a?(Hash)
135+
136+
certs = {:signing => [], :encryption => [] }
137+
138+
if idp_cert_multi.key?(:signing) and not idp_cert_multi[:signing].empty?
139+
idp_cert_multi[:signing].each do |idp_cert|
140+
formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
141+
certs[:signing].push(OpenSSL::X509::Certificate.new(formatted_cert))
142+
end
143+
end
144+
145+
if idp_cert_multi.key?(:encryption) and not idp_cert_multi[:encryption].empty?
146+
idp_cert_multi[:encryption].each do |idp_cert|
147+
formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
148+
certs[:encryption].push(OpenSSL::X509::Certificate.new(formatted_cert))
149+
end
150+
end
151+
152+
certs
153+
end
154+
128155
# @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
129156
#
130157
def get_sp_cert

lib/onelogin/ruby-saml/slo_logoutrequest.rb

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class SloLogoutrequest < SamlMessage
2626
# @param request [String] A UUEncoded Logout Request from the IdP.
2727
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
2828
# Or :allowed_clock_drift for the logout request validation process to allow a clock drift when checking dates with
29+
# Or :relax_signature_validation to accept signatures if no idp certificate registered on settings
2930
#
3031
# @raise [ArgumentError] If Request is nil
3132
#
@@ -227,7 +228,13 @@ def validate_signature
227228
return true if options.nil?
228229
return true unless options.has_key? :get_params
229230
return true unless options[:get_params].has_key? 'Signature'
230-
return true if settings.get_idp_cert.nil?
231+
232+
idp_cert = settings.get_idp_cert
233+
idp_certs = settings.get_idp_cert_multi
234+
235+
if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?)
236+
return options.has_key? :relax_signature_validation
237+
end
231238

232239
query_string = OneLogin::RubySaml::Utils.build_query(
233240
:type => 'SAMLRequest',
@@ -236,12 +243,27 @@ def validate_signature
236243
:sig_alg => options[:get_params]['SigAlg']
237244
)
238245

239-
valid = OneLogin::RubySaml::Utils.verify_signature(
240-
:cert => settings.get_idp_cert,
241-
:sig_alg => options[:get_params]['SigAlg'],
242-
:signature => options[:get_params]['Signature'],
243-
:query_string => query_string
244-
)
246+
if idp_certs.nil? || idp_certs[:signing].empty?
247+
valid = OneLogin::RubySaml::Utils.verify_signature(
248+
:cert => settings.get_idp_cert,
249+
:sig_alg => options[:get_params]['SigAlg'],
250+
:signature => options[:get_params]['Signature'],
251+
:query_string => query_string
252+
)
253+
else
254+
valid = false
255+
idp_certs[:signing].each do |idp_cert|
256+
valid = OneLogin::RubySaml::Utils.verify_signature(
257+
:cert => idp_cert,
258+
:sig_alg => options[:get_params]['SigAlg'],
259+
:signature => options[:get_params]['Signature'],
260+
:query_string => query_string
261+
)
262+
if valid
263+
break
264+
end
265+
end
266+
end
245267

246268
unless valid
247269
return append_error("Invalid Signature on Logout Request")

lib/xml_security.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,31 @@ def validate_document(idp_cert_fingerprint, soft = true, options = {})
240240
validate_signature(base64_cert, soft)
241241
end
242242

243+
def validate_document_with_cert(idp_cert)
244+
# get cert from response
245+
cert_element = REXML::XPath.first(
246+
self,
247+
"//ds:X509Certificate",
248+
{ "ds"=>DSIG }
249+
)
250+
251+
if cert_element
252+
base64_cert = cert_element.text
253+
cert_text = Base64.decode64(base64_cert)
254+
begin
255+
cert = OpenSSL::X509::Certificate.new(cert_text)
256+
rescue OpenSSL::X509::CertificateError => e
257+
return append_error("Certificate Error", soft)
258+
end
259+
260+
# check saml response cert matches provided idp cert
261+
if idp_cert.to_pem != cert.to_pem
262+
return false
263+
end
264+
validate_signature(base64_cert, true)
265+
end
266+
end
267+
243268
def validate_signature(base64_cert, soft = true)
244269

245270
document = Nokogiri::XML(self.to_s) do |config|

test/logoutresponse_test.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,27 @@ class RubySamlTest < Minitest::Test
223223
settings.idp_cert = ruby_saml_cert_text
224224
end
225225

226+
it "return true when no idp_cert is provided and option :relax_signature_validation is present" do
227+
settings.idp_cert = nil
228+
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1
229+
params['RelayState'] = params[:RelayState]
230+
options = {}
231+
options[:get_params] = params
232+
options[:relax_signature_validation] = true
233+
logoutresponse_sign_test = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options)
234+
assert logoutresponse_sign_test.send(:validate_signature)
235+
end
236+
237+
it "return false when no idp_cert is provided and no option :relax_signature_validation is present" do
238+
settings.idp_cert = nil
239+
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1
240+
params['RelayState'] = params[:RelayState]
241+
options = {}
242+
options[:get_params] = params
243+
logoutresponse_sign_test = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options)
244+
assert !logoutresponse_sign_test.send(:validate_signature)
245+
end
246+
226247
it "return true when valid RSA_SHA1 Signature" do
227248
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1
228249
params['RelayState'] = params[:RelayState]
@@ -262,6 +283,45 @@ class RubySamlTest < Minitest::Test
262283
assert logoutresponse.errors.include? "Invalid Signature on Logout Response"
263284
end
264285
end
286+
287+
describe "#validate_signature" do
288+
let (:params) { OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, random_id, "Custom Logout Message", :RelayState => 'http://example.com') }
289+
290+
before do
291+
settings.soft = true
292+
settings.idp_slo_target_url = "http://example.com?field=value"
293+
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1
294+
settings.security[:logout_responses_signed] = true
295+
settings.security[:embed_sign] = false
296+
settings.certificate = ruby_saml_cert_text
297+
settings.private_key = ruby_saml_key_text
298+
settings.idp_cert = nil
299+
end
300+
301+
it "return true when at least a idp_cert is valid" do
302+
params['RelayState'] = params[:RelayState]
303+
options = {}
304+
options[:get_params] = params
305+
settings.idp_cert_multi = {
306+
:signing => [ruby_saml_cert_text2, ruby_saml_cert_text],
307+
:encryption => []
308+
}
309+
logoutresponse_sign_test = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options)
310+
assert logoutresponse_sign_test.send(:validate_signature)
311+
end
312+
313+
it "return false when none cert on idp_cert_multi is valid" do
314+
params['RelayState'] = params[:RelayState]
315+
options = {}
316+
options[:get_params] = params
317+
settings.idp_cert_multi = {
318+
:signing => [ruby_saml_cert_text2, ruby_saml_cert_text2],
319+
:encryption => []
320+
}
321+
logoutresponse_sign_test = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options)
322+
assert !logoutresponse_sign_test.send(:validate_signature)
323+
end
324+
end
265325
end
266326
end
267327
end

test/response_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,29 @@ class RubySamlTest < Minitest::Test
799799
end
800800
end
801801

802+
describe "#validate_signature with multiple idp certs" do
803+
it "return true when at least a cert on idp_cert_multi is valid" do
804+
settings.idp_cert_multi = {
805+
:signing => [ruby_saml_cert_text2, ruby_saml_cert_text],
806+
:encryption => []
807+
}
808+
response_valid_signed.settings = settings
809+
assert response_valid_signed.send(:validate_signature)
810+
assert_empty response_valid_signed.errors
811+
end
812+
813+
it "return false when none cert on idp_cert_multi is valid" do
814+
settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
815+
settings.idp_cert_multi = {
816+
:signing => [ruby_saml_cert_text2, ruby_saml_cert_text2],
817+
:encryption => []
818+
}
819+
response_valid_signed.settings = settings
820+
assert !response_valid_signed.send(:validate_signature)
821+
assert_includes response_valid_signed.errors, "Invalid Signature on SAML Response"
822+
end
823+
end
824+
802825
describe "#validate nameid" do
803826
it "return false when no nameid element and required by settings" do
804827
settings.security[:want_name_id] = true

0 commit comments

Comments
 (0)