From a2a0002d5f6d9a56e77ad1ca6c4e307d266843b8 Mon Sep 17 00:00:00 2001 From: johnnyshields <27655+johnnyshields@users.noreply.github.com> Date: Mon, 27 Nov 2023 01:10:39 +0900 Subject: [PATCH] Add sp_cert_multi to facilitate key rotation --- CHANGELOG.md | 11 +- README.md | 65 ++-- lib/onelogin/ruby-saml/authrequest.rb | 13 +- lib/onelogin/ruby-saml/logoutrequest.rb | 10 +- lib/onelogin/ruby-saml/metadata.rb | 44 ++- lib/onelogin/ruby-saml/response.rb | 36 +-- lib/onelogin/ruby-saml/settings.rb | 160 ++++++++-- lib/onelogin/ruby-saml/slo_logoutrequest.rb | 10 +- lib/onelogin/ruby-saml/slo_logoutresponse.rb | 12 +- lib/onelogin/ruby-saml/utils.rb | 72 ++++- test/helpers/certificate_helper.rb | 44 +++ test/logoutrequest_test.rb | 97 +++++- test/logoutresponse_test.rb | 4 +- test/metadata_test.rb | 33 +- test/request_test.rb | 79 ++++- test/response_test.rb | 32 +- test/settings_test.rb | 301 ++++++++++++++++++- test/slo_logoutrequest_test.rb | 9 +- test/slo_logoutresponse_test.rb | 87 +++++- test/test_helper.rb | 1 + test/utils_test.rb | 163 +++++++++- 21 files changed, 1124 insertions(+), 159 deletions(-) create mode 100644 test/helpers/certificate_helper.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d43889f..f5e2b1dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,20 @@ # Ruby SAML Changelog + +### 1.17.0 +* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Add `Settings#sp_cert_multi` paramter to facilitate SP certificate and key rotation. +* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Support multiple simultaneous SP decryption keys via `Settings#sp_cert_multi` parameter. +* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Deprecate `Settings#certificate_new` parameter. +* [#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. +* [#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`. +* [#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. + ### 1.16.0 (Oct 09, 2023) * [#671](https://github.com/SAML-Toolkits/ruby-saml/pull/671) Add support on LogoutRequest with Encrypted NameID ### 1.15.0 (Jan 04, 2023) * [#650](https://github.com/SAML-Toolkits/ruby-saml/pull/650) Replace strip! by strip on compute_digest method * [#638](https://github.com/SAML-Toolkits/ruby-saml/pull/638) Fix dateTime format for the validUntil attribute of the generated metadata -* [#576](https://github.com/SAML-Toolkits/ruby-saml/pull/576) Support idp cert multi with string keys +* [#576](https://github.com/SAML-Toolkits/ruby-saml/pull/576) Support `Settings#idp_cert_multi` with string keys * [#567](https://github.com/SAML-Toolkits/ruby-saml/pull/567) Improve Code quality * Add info about new repo, new maintainer, new security contact * Fix tests, Adjust dependencies, Add ruby 3.2 and new jruby versions tests to the CI. Add coveralls support diff --git a/README.md b/README.md index 3ed72ef4..229799ef 100644 --- a/README.md +++ b/README.md @@ -735,6 +735,48 @@ validation fails. You may disable such exceptions using the `settings.security[: settings.security[:soft] = true # Do not raise error on failed signature/certificate validations ``` +#### Advanced SP Certificate Usage & Key Rollover + +Ruby SAML provides the `settings.sp_cert_multi` parameter to enable the following +advanced usage scenarios: +- Rotating SP certificates and private keys without disruption of service. +- Specifying separate SP certificates for signing and encryption. + +The `sp_cert_multi` parameter replaces `certificate` and `private_key` +(you may not specify both pparameters at the same time.) `sp_cert_multi` has the following shape: + +```ruby +settings.sp_cert_multi = { + signing: [ + { certificate: cert1, private_key: private_key1 }, + { certificate: cert2, private_key: private_key2 } + ], + encryption: [ + { certificate: cert1, private_key: private_key1 }, + { certificate: cert3, private_key: private_key1 } + ], +} +``` + +Certificate rotation is acheived by inserting new certificates at the bottom of each list, +and then removing the old certificates from the top of the list once your IdPs have migrated. +A common practice is for apps to publish the current SP metadata at a URL endpoint and have +the IdP regularly poll for updates. + +Note the following: +- You may re-use the same certificate and/or private key in multiple places, including for both signing and encryption. +- The IdP should attempt to verify signatures with *all* `:signing` certificates, + and permit if *any one* succeeds. When signing, Ruby SAML will use the first SP certificate + in the `sp_cert_multi[:signing]` array. This will be the first active/non-expired certificate + in the array if `settings.security[:check_sp_cert_expiration]` is true. +- The IdP may encrypt with any of the SP certificates in the `sp_cert_multi[:encryption]` + array. When decrypting, Ruby SAML attempt to decrypt with each SP private key in + `sp_cert_multi[:encryption]` until the decryption is successful. This will skip private + keys for inactive/expired certificates if `:check_sp_cert_expiration` is true. +- If `:check_sp_cert_expiration` is true, the generated SP metadata XML will not include + inactive/expired certificates. This avoids validation errors when the IdP reads the SP + metadata. + #### Audience Validation A service provider should only consider a SAML response valid if the IdP includes an @@ -758,29 +800,6 @@ is invalid using the `settings.security[:strict_audience_validation]` parameter. settings.security[:strict_audience_validation] = true ``` -#### Key Rollover - -To update the SP X.509 certificate and private key without disruption of service, you may define the parameter -`settings.certificate_new`. This will publish the new SP certificate in your metadata so that your IdP counterparties -may cache it in preparation for rollover. - -For example, if you to rollover from `CERT A` to `CERT B`. Before rollover, your settings should look as follows. -Both `CERT A` and `CERT B` will now appear in your SP metadata, however `CERT A` will still be used for signing -and encryption at this time. - -```ruby - settings.certificate = "CERT A" - settings.private_key = "PRIVATE KEY FOR CERT A" - settings.certificate_new = "CERT B" -``` - -After the IdP has cached `CERT B`, you may then change your settings as follows: - -```ruby - settings.certificate = "CERT B" - settings.private_key = "PRIVATE KEY FOR CERT B" -``` - ## Single Log Out Ruby SAML supports SP-initiated Single Logout and IdP-Initiated Single Logout. diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index d604eac6..0aadff2f 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -72,9 +72,10 @@ def create_params(settings, params={}) request = deflate(request) if settings.compress_request base64_request = encode(request) request_params = {"SAMLRequest" => base64_request} + sp_signing_key = settings.get_sp_signing_key - if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && settings.private_key - params['SigAlg'] = settings.security[:signature_method] + if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && sp_signing_key + params['SigAlg'] = settings.security[:signature_method] url_string = OneLogin::RubySaml::Utils.build_query( :type => 'SAMLRequest', :data => base64_request, @@ -82,7 +83,7 @@ def create_params(settings, params={}) :sig_alg => params['SigAlg'] ) sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, url_string) + signature = sp_signing_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end @@ -179,15 +180,13 @@ def create_xml_document(settings) end def sign_document(document, settings) - if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && settings.private_key && settings.certificate - private_key = settings.get_sp_key - cert = settings.get_sp_cert + cert, private_key = settings.get_sp_signing_pair + if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && private_key && cert document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end document end - end end end diff --git a/lib/onelogin/ruby-saml/logoutrequest.rb b/lib/onelogin/ruby-saml/logoutrequest.rb index c6d96da0..d2bb21ec 100644 --- a/lib/onelogin/ruby-saml/logoutrequest.rb +++ b/lib/onelogin/ruby-saml/logoutrequest.rb @@ -69,8 +69,9 @@ def create_params(settings, params={}) request = deflate(request) if settings.compress_request base64_request = encode(request) request_params = {"SAMLRequest" => base64_request} + sp_signing_key = settings.get_sp_signing_key - if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && settings.private_key + if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && sp_signing_key params['SigAlg'] = settings.security[:signature_method] url_string = OneLogin::RubySaml::Utils.build_query( :type => 'SAMLRequest', @@ -79,7 +80,7 @@ def create_params(settings, params={}) :sig_alg => params['SigAlg'] ) sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, url_string) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end @@ -138,9 +139,8 @@ def create_xml_document(settings) def sign_document(document, settings) # embed signature - if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && settings.private_key && settings.certificate - private_key = settings.get_sp_key - cert = settings.get_sp_cert + cert, private_key = settings.get_sp_signing_pair + if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && private_key && cert document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end diff --git a/lib/onelogin/ruby-saml/metadata.rb b/lib/onelogin/ruby-saml/metadata.rb index e6b5bdae..fed96b67 100644 --- a/lib/onelogin/ruby-saml/metadata.rb +++ b/lib/onelogin/ruby-saml/metadata.rb @@ -62,29 +62,14 @@ def add_sp_sso_element(root, settings) } end - # Add KeyDescriptor if messages will be signed / encrypted - # with SP certificate, and new SP certificate if any + # Add KeyDescriptor elements for SP certificates. def add_sp_certificates(sp_sso, settings) - cert = settings.get_sp_cert - cert_new = settings.get_sp_cert_new - - for sp_cert in [cert, cert_new] - if sp_cert - cert_text = Base64.encode64(sp_cert.to_der).gsub("\n", '') - kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" } - ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} - xd = ki.add_element "ds:X509Data" - xc = xd.add_element "ds:X509Certificate" - xc.text = cert_text - - if settings.security[:want_assertions_encrypted] - kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" } - ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} - xd2 = ki2.add_element "ds:X509Data" - xc2 = xd2.add_element "ds:X509Certificate" - xc2.text = cert_text - end - end + certs = settings.get_sp_certs + + certs[:signing].each { |cert, _| add_sp_cert_element(sp_sso, cert, :signing) } + + if settings.security[:want_assertions_encrypted] + certs[:encryption].each { |cert, _| add_sp_cert_element(sp_sso, cert, :encryption) } end sp_sso @@ -153,8 +138,7 @@ def add_extras(root, _settings) def embed_signature(meta_doc, settings) return unless settings.security[:metadata_signed] - private_key = settings.get_sp_key - cert = settings.get_sp_cert + cert, private_key = settings.get_sp_signing_pair return unless private_key && cert meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) @@ -172,6 +156,18 @@ def output_xml(meta_doc, pretty_print) ret end + + private + + def add_sp_cert_element(sp_sso, cert, use) + return unless cert + cert_text = Base64.encode64(cert.to_der).gsub("\n", '') + kd = sp_sso.add_element "md:KeyDescriptor", { "use" => use.to_s } + ki = kd.add_element "ds:KeyInfo", { "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#" } + xd = ki.add_element "ds:X509Data" + xc = xd.add_element "ds:X509Certificate" + xc.text = cert_text + end end end end diff --git a/lib/onelogin/ruby-saml/response.rb b/lib/onelogin/ruby-saml/response.rb index b356f0a2..29d48594 100644 --- a/lib/onelogin/ruby-saml/response.rb +++ b/lib/onelogin/ruby-saml/response.rb @@ -915,9 +915,9 @@ def name_id_node begin encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID') if encrypted_node - node = decrypt_nameid(encrypted_node) + decrypt_nameid(encrypted_node) else - node = xpath_first_from_signed_assertion('/a:Subject/a:NameID') + xpath_first_from_signed_assertion('/a:Subject/a:NameID') end end end @@ -969,7 +969,7 @@ def xpath_from_signed_assertion(subelt=nil) # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted # def generate_decrypted_document - if settings.nil? || !settings.get_sp_key + if settings.nil? || settings.get_sp_decryption_keys.empty? 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') end @@ -1012,42 +1012,42 @@ def decrypt_assertion(encrypted_assertion_node) end # Decrypts an EncryptedID element - # @param encryptedid_node [REXML::Element] The EncryptedID element + # @param encrypted_id_node [REXML::Element] The EncryptedID element # @return [REXML::Document] The decrypted EncrypedtID element # - def decrypt_nameid(encryptedid_node) - decrypt_element(encryptedid_node, /(.*<\/(\w+:)?NameID>)/m) + def decrypt_nameid(encrypted_id_node) + decrypt_element(encrypted_id_node, /(.*<\/(\w+:)?NameID>)/m) end - # Decrypts an EncryptedID element - # @param encryptedid_node [REXML::Element] The EncryptedID element - # @return [REXML::Document] The decrypted EncrypedtID element + # Decrypts an EncryptedAttribute element + # @param encrypted_attribute_node [REXML::Element] The EncryptedAttribute element + # @return [REXML::Document] The decrypted EncryptedAttribute element # - def decrypt_attribute(encryptedattribute_node) - decrypt_element(encryptedattribute_node, /(.*<\/(\w+:)?Attribute>)/m) + def decrypt_attribute(encrypted_attribute_node) + decrypt_element(encrypted_attribute_node, /(.*<\/(\w+:)?Attribute>)/m) end # Decrypt an element - # @param encryptedid_node [REXML::Element] The encrypted element - # @param rgrex string Regex + # @param encrypt_node [REXML::Element] The encrypted element + # @param regexp [Regexp] The regular expression to extract the decrypted data # @return [REXML::Document] The decrypted element # - def decrypt_element(encrypt_node, rgrex) - if settings.nil? || !settings.get_sp_key + def decrypt_element(encrypt_node, regexp) + if settings.nil? || settings.get_sp_decryption_keys.empty? raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it') end - if encrypt_node.name == 'EncryptedAttribute' node_header = '' else node_header = '' end - elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key) + elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypt_node, settings.get_sp_decryption_keys) + # If we get some problematic noise in the plaintext after decrypting. # This quick regexp parse will grab only the Element and discard the noise. - elem_plaintext = elem_plaintext.match(rgrex)[0] + elem_plaintext = elem_plaintext.match(regexp)[0] # To avoid namespace errors if saml namespace is not defined # create a parent node first with the namespace defined diff --git a/lib/onelogin/ruby-saml/settings.rb b/lib/onelogin/ruby-saml/settings.rb index 1c61fdef..401732ac 100644 --- a/lib/onelogin/ruby-saml/settings.rb +++ b/lib/onelogin/ruby-saml/settings.rb @@ -60,8 +60,8 @@ def initialize(overrides = {}, keep_security_attributes = false) attr_accessor :attributes_index attr_accessor :force_authn attr_accessor :certificate - attr_accessor :certificate_new attr_accessor :private_key + attr_accessor :sp_cert_multi attr_accessor :authn_context attr_accessor :authn_context_comparison attr_accessor :authn_context_decl_ref @@ -70,6 +70,7 @@ def initialize(overrides = {}, keep_security_attributes = false) attr_accessor :security attr_accessor :soft # Deprecated + attr_accessor :certificate_new attr_accessor :assertion_consumer_logout_service_url attr_reader :assertion_consumer_logout_service_binding attr_accessor :issuer @@ -180,10 +181,7 @@ def get_fingerprint # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it) # def get_idp_cert - return nil if idp_cert.nil? || idp_cert.empty? - - formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert) - OpenSSL::X509::Certificate.new(formatted_cert) + OneLogin::RubySaml::Utils.build_cert_object(idp_cert) end # @return [Hash with 2 arrays of OpenSSL::X509::Certificate] Build multiple IdP certificates from the settings. @@ -191,7 +189,7 @@ def get_idp_cert def get_idp_cert_multi return nil if idp_cert_multi.nil? || idp_cert_multi.empty? - raise ArgumentError.new("Invalid value for idp_cert_multi") if not idp_cert_multi.is_a?(Hash) + raise ArgumentError.new("Invalid value for idp_cert_multi") unless idp_cert_multi.is_a?(Hash) certs = {:signing => [], :encryption => [] } @@ -200,49 +198,70 @@ def get_idp_cert_multi next if !certs_for_type || certs_for_type.empty? certs_for_type.each do |idp_cert| - formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert) - certs[type].push(OpenSSL::X509::Certificate.new(formatted_cert)) + certs[type].push(OneLogin::RubySaml::Utils.build_cert_object(idp_cert)) end end certs end - # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it) - # - def get_sp_cert - return nil if certificate.nil? || certificate.empty? + # @return [Hash>>] + # Build the SP certificates and private keys from the settings. If + # check_sp_cert_expiration is true, only returns certificates and private keys + # that are not expired. + def get_sp_certs + certs = get_all_sp_certs + return certs unless security[:check_sp_cert_expiration] - formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate) - cert = OpenSSL::X509::Certificate.new(formatted_cert) + active_certs = { signing: [], encryption: [] } + certs.each do |use, pairs| + next if pairs.empty? - if security[:check_sp_cert_expiration] - if OneLogin::RubySaml::Utils.is_cert_expired(cert) - raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.") - end + pairs = pairs.select { |cert, _| !cert || OneLogin::RubySaml::Utils.is_cert_active(cert) } + raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.") if pairs.empty? + + active_certs[use] = pairs.freeze end + active_certs.freeze + end - cert + # @return [Array] + # The SP signing certificate and private key. + def get_sp_signing_pair + get_sp_certs[:signing].first end - # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it) - # - def get_sp_cert_new - return nil if certificate_new.nil? || certificate_new.empty? + # @return [OpenSSL::X509::Certificate] The SP signing certificate. + # @deprecated Use get_sp_signing_pair or get_sp_certs instead. + def get_sp_cert + node = get_sp_signing_pair + node[0] if node + end - formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new) - OpenSSL::X509::Certificate.new(formatted_cert) + # @return [OpenSSL::PKey::RSA] The SP signing key. + def get_sp_signing_key + node = get_sp_signing_pair + node[1] if node end - # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it) - # - def get_sp_key - return nil if private_key.nil? || private_key.empty? + # @deprecated Use get_sp_signing_key or get_sp_certs instead. + alias_method :get_sp_key, :get_sp_signing_key - formatted_private_key = OneLogin::RubySaml::Utils.format_private_key(private_key) - OpenSSL::PKey::RSA.new(formatted_private_key) + # @return [Array] The SP decryption keys. + def get_sp_decryption_keys + ary = get_sp_certs[:encryption].map { |pair| pair[1] } + ary.compact! + ary.uniq!(&:to_pem) + ary.freeze end + # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings. + # + # @deprecated Use get_sp_certs instead + def get_sp_cert_new + node = get_sp_certs[:signing].last + node[0] if node + end def idp_binding_from_embed_sign security[:embed_sign] ? Utils::BINDINGS[:post] : Utils::BINDINGS[:redirect] @@ -280,6 +299,85 @@ def get_binding(value) :lowercase_url_encoding => false }.freeze }.freeze + + private + + # @return [Hash>>] + # Build the SP certificates and private keys from the settings. Returns all + # certificates and private keys, even if they are expired. + def get_all_sp_certs + validate_sp_certs_params! + get_sp_certs_multi || get_sp_certs_single + end + + # Validate certificate, certificate_new, private_key, and sp_cert_multi params. + def validate_sp_certs_params! + multi = sp_cert_multi && !sp_cert_multi.empty? + cert = certificate && !certificate.empty? + cert_new = certificate_new && !certificate_new.empty? + pk = private_key && !private_key.empty? + if multi && (cert || cert_new || pk) + raise ArgumentError.new("Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters") + end + end + + # Get certs from certificate, certificate_new, and private_key parameters. + def get_sp_certs_single + certs = { :signing => [], :encryption => [] } + + sp_key = OneLogin::RubySaml::Utils.build_private_key_object(private_key) + cert = OneLogin::RubySaml::Utils.build_cert_object(certificate) + if cert || sp_key + ary = [cert, sp_key].freeze + certs[:signing] << ary + certs[:encryption] << ary + end + + cert_new = OneLogin::RubySaml::Utils.build_cert_object(certificate_new) + if cert_new + ary = [cert_new, sp_key].freeze + certs[:signing] << ary + certs[:encryption] << ary + end + + certs + end + + # Get certs from get_sp_cert_multi parameter. + def get_sp_certs_multi + return if sp_cert_multi.nil? || sp_cert_multi.empty? + + raise ArgumentError.new("sp_cert_multi must be a Hash") unless sp_cert_multi.is_a?(Hash) + + certs = { :signing => [], :encryption => [] }.freeze + + [:signing, :encryption].each do |type| + certs_for_type = sp_cert_multi[type] || sp_cert_multi[type.to_s] + next if !certs_for_type || certs_for_type.empty? + + unless certs_for_type.is_a?(Array) && certs_for_type.all? { |cert| cert.is_a?(Hash) } + raise ArgumentError.new("sp_cert_multi :#{type} node must be an Array of Hashes") + end + + certs_for_type.each do |pair| + cert = pair[:certificate] || pair['certificate'] || pair[:cert] || pair['cert'] + key = pair[:private_key] || pair['private_key'] || pair[:key] || pair['key'] + + unless cert && key + raise ArgumentError.new("sp_cert_multi :#{type} node Hashes must specify keys :certificate and :private_key") + end + + certs[type] << [ + OneLogin::RubySaml::Utils.build_cert_object(cert), + OneLogin::RubySaml::Utils.build_private_key_object(key) + ].freeze + end + end + + certs.each { |_, ary| ary.freeze } + certs + end end end end + diff --git a/lib/onelogin/ruby-saml/slo_logoutrequest.rb b/lib/onelogin/ruby-saml/slo_logoutrequest.rb index 6c4a8867..10a500f3 100644 --- a/lib/onelogin/ruby-saml/slo_logoutrequest.rb +++ b/lib/onelogin/ruby-saml/slo_logoutrequest.rb @@ -91,16 +91,16 @@ def name_id_node end # Decrypts an EncryptedID element - # @param encryptedid_node [REXML::Element] The EncryptedID element + # @param encrypted_id_node [REXML::Element] The EncryptedID element # @return [REXML::Document] The decrypted EncrypedtID element # - def decrypt_nameid(encrypt_node) + def decrypt_nameid(encrypted_id_node) - if settings.nil? || !settings.get_sp_key - raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it') + if settings.nil? || settings.get_sp_decryption_keys.empty? + raise ValidationError.new('An ' + encrypted_id_node.name + ' found and no SP private key found on the settings to decrypt it') end - elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key) + elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypted_id_node, settings.get_sp_decryption_keys) # If we get some problematic noise in the plaintext after decrypting. # This quick regexp parse will grab only the Element and discard the noise. elem_plaintext = elem_plaintext.match(/(.*<\/(\w+:)?NameID>)/m)[0] diff --git a/lib/onelogin/ruby-saml/slo_logoutresponse.rb b/lib/onelogin/ruby-saml/slo_logoutresponse.rb index b3f2243d..c2c73d0c 100644 --- a/lib/onelogin/ruby-saml/slo_logoutresponse.rb +++ b/lib/onelogin/ruby-saml/slo_logoutresponse.rb @@ -78,9 +78,10 @@ def create_params(settings, request_id = nil, logout_message = nil, params = {}, response = deflate(response) if settings.compress_response base64_response = encode(response) response_params = {"SAMLResponse" => base64_response} + sp_signing_key = settings.get_sp_signing_key - if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && settings.private_key - params['SigAlg'] = settings.security[:signature_method] + if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && sp_signing_key + params['SigAlg'] = settings.security[:signature_method] url_string = OneLogin::RubySaml::Utils.build_query( :type => 'SAMLResponse', :data => base64_response, @@ -88,7 +89,7 @@ def create_params(settings, request_id = nil, logout_message = nil, params = {}, :sig_alg => params['SigAlg'] ) sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, url_string) + signature = sp_signing_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end @@ -150,9 +151,8 @@ def create_xml_document(settings, request_id = nil, logout_message = nil, status def sign_document(document, settings) # embed signature - if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.private_key && settings.certificate - private_key = settings.get_sp_key - cert = settings.get_sp_cert + cert, private_key = settings.get_sp_signing_pair + if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && private_key && cert document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end diff --git a/lib/onelogin/ruby-saml/utils.rb b/lib/onelogin/ruby-saml/utils.rb index 3a5915ba..5756e696 100644 --- a/lib/onelogin/ruby-saml/utils.rb +++ b/lib/onelogin/ruby-saml/utils.rb @@ -34,16 +34,24 @@ class Utils $)x.freeze UUID_PREFIX = '_' - # Checks if the x509 cert provided is expired - # - # @param cert [Certificate] The x509 certificate + # Checks if the x509 cert provided is expired. # + # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate. + # @return [true|false] Whether the certificate is expired. def self.is_cert_expired(cert) - if cert.is_a?(String) - cert = OpenSSL::X509::Certificate.new(cert) - end + cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String) + + cert.not_after < Time.now + end - return cert.not_after < Time.now + # Checks if the x509 cert provided has both started and has not expired. + # + # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate. + # @return [true|false] Whether the certificate is currently active. + def self.is_cert_active(cert) + cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String) + now = Time.now + cert.not_before <= now && cert.not_after >= now end # Interprets a ISO8601 duration value relative to a given timestamp. @@ -128,6 +136,28 @@ def self.format_private_key(key) "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----" end + # Given a certificate string, return an OpenSSL::X509::Certificate object. + # + # @param cert [String] The original certificate + # @return [OpenSSL::X509::Certificate] The certificate object + # + def self.build_cert_object(cert) + return nil if cert.nil? || cert.empty? + + OpenSSL::X509::Certificate.new(format_cert(cert)) + end + + # Given a private key string, return an OpenSSL::PKey::RSA object. + # + # @param cert [String] The original private key + # @return [OpenSSL::PKey::RSA] The private key object + # + def self.build_private_key_object(private_key) + return nil if private_key.nil? || private_key.empty? + + OpenSSL::PKey::RSA.new(format_private_key(private_key)) + end + # Build the Query String signature that will be used in the HTTP-Redirect binding # to generate the Signature # @param params [Hash] Parameters to build the Query String @@ -199,7 +229,7 @@ def self.escape_request_param(param, lowercase_url_encoding) # Validate the Signature parameter sent on the HTTP-Redirect binding # @param params [Hash] Parameters to be used in the validation process - # @option params [OpenSSL::X509::Certificate] cert The Identity provider public certtificate + # @option params [OpenSSL::X509::Certificate] cert The IDP public certificate # @option params [String] sig_alg The SigAlg parameter # @option params [String] signature The Signature parameter (base64 encoded) # @option params [String] query_string The full GET Query String to be compared @@ -236,9 +266,29 @@ def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil error_msg end + # Obtains the decrypted string from an Encrypted node element in XML, + # given multiple private keys to try. + # @param encrypted_node [REXML::Element] The Encrypted element + # @param private_keys [Array] The Service provider private key + # @return [String] The decrypted data + def self.decrypt_multi(encrypted_node, private_keys) + raise ArgumentError.new('private_keys must be specified') if !private_keys || private_keys.empty? + + error = nil + private_keys.each do |key| + begin + return decrypt_data(encrypted_node, key) + rescue OpenSSL::PKey::PKeyError => e + error ||= e + end + end + + raise(error) if error + end + # Obtains the decrypted string from an Encrypted node element in XML - # @param encrypted_node [REXML::Element] The Encrypted element - # @param private_key [OpenSSL::PKey::RSA] The Service provider private key + # @param encrypted_node [REXML::Element] The Encrypted element + # @param private_key [OpenSSL::PKey::RSA] The Service provider private key # @return [String] The decrypted data def self.decrypt_data(encrypted_node, private_key) encrypt_data = REXML::XPath.first( @@ -302,7 +352,7 @@ def self.retrieve_symetric_key_reference(encrypt_data) # Obtains the deciphered text # @param cipher_text [String] The ciphered text - # @param symmetric_key [String] The symetric key used to encrypt the text + # @param symmetric_key [String] The symmetric key used to encrypt the text # @param algorithm [String] The encrypted algorithm # @return [String] The deciphered text def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm) diff --git a/test/helpers/certificate_helper.rb b/test/helpers/certificate_helper.rb new file mode 100644 index 00000000..e826fbfb --- /dev/null +++ b/test/helpers/certificate_helper.rb @@ -0,0 +1,44 @@ +require 'openssl' + +module CertificateHelper + extend self + + def generate_pair(not_before: nil, not_after: nil) + key = generate_key + cert = generate_cert(key, not_before: not_before, not_after: not_after) + [cert, key] + end + + def generate_pair_hash(not_before: nil, not_after: nil) + cert, key = generate_pair(not_before: not_before, not_after: not_after) + { certificate: cert.to_pem, private_key: key.to_pem } + end + + def generate_key + OpenSSL::PKey::RSA.new(1024) + end + + def generate_cert(key = generate_key, not_before: nil, not_after: nil) + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = 0 + cert.not_before = not_before || Time.now - one_year + cert.not_after = not_after || Time.now + one_year + cert.public_key = key.public_key + cert.subject = OpenSSL::X509::Name.parse "/DC=org/DC=ruby-saml/CN=Ruby SAML CA" + cert.issuer = cert.subject # self-signed + factory = OpenSSL::X509::ExtensionFactory.new + factory.subject_certificate = cert + factory.issuer_certificate = cert + cert.add_extension factory.create_extension("basicConstraints","CA:TRUE", true) + cert.add_extension factory.create_extension("keyUsage","keyCertSign, cRLSign", true) + cert.sign(key, OpenSSL::Digest::SHA1.new) + cert + end + + private + + def one_year + 3600 * 24 * 365 + end +end diff --git a/test/logoutrequest_test.rb b/test/logoutrequest_test.rb index fe32e220..08a15976 100644 --- a/test/logoutrequest_test.rb +++ b/test/logoutrequest_test.rb @@ -110,7 +110,6 @@ class RequestTest < Minitest::Test end describe "signing with HTTP-POST binding" do - before do settings.security[:logout_requests_signed] = true settings.idp_slo_service_binding = :post @@ -153,7 +152,7 @@ class RequestTest < Minitest::Test assert_match %r[], inflated end - it "created a signed logout request" do + it "create a signed logout request" do settings.compress_request = true unauth_req = OneLogin::RubySaml::Logoutrequest.new @@ -165,6 +164,17 @@ class RequestTest < Minitest::Test assert_match %r[], inflated end + it "create an uncompressed signed logout request" do + settings.compress_request = false + + params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match %r[], request_xml + assert_match %r[], request_xml + end + it "create a signed logout request with 256 digest and signature method" do settings.compress_request = false settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 @@ -172,7 +182,6 @@ class RequestTest < Minitest::Test params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) - assert_match %r[([a-zA-Z0-9/+=]+)], request_xml assert_match %r[], request_xml assert_match %r[], request_xml @@ -190,6 +199,53 @@ class RequestTest < Minitest::Test assert_match %r[], request_xml assert_match %r[], request_xml end + + it "create a signed logout request using the first certificate and key" do + settings.compress_request = false + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match %r[], request_xml + assert_match %r[], request_xml + end + + it "create a signed logout request using the first valid certificate and key when :check_sp_cert_expiration is true" do + settings.compress_request = false + settings.certificate = nil + settings.private_key = nil + settings.security[:check_sp_cert_expiration] = true + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match %r[], request_xml + assert_match %r[], request_xml + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + end + end end describe "signing with HTTP-Redirect binding" do @@ -269,6 +325,41 @@ class RequestTest < Minitest::Test assert_equal signature_algorithm, OpenSSL::Digest::SHA512 assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) end + + it "create a signature parameter using the first certificate and key" do + settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 + settings.compress_request = false + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['SAMLRequest'] + assert params[:RelayState] + assert params['Signature'] + assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1 + + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + + signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA1 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + end + end end describe "DEPRECATED: signing with HTTP-POST binding via :embed_sign" do diff --git a/test/logoutresponse_test.rb b/test/logoutresponse_test.rb index 22d2347b..7cc79991 100644 --- a/test/logoutresponse_test.rb +++ b/test/logoutresponse_test.rb @@ -315,7 +315,7 @@ class RubySamlTest < Minitest::Test assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, query) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = Base64.encode64(signature).gsub(/\n/, "") # Re-create the Logoutresponse based on these modified parameters, # and ask it to validate the signature. It will do it incorrectly, @@ -351,7 +351,7 @@ class RubySamlTest < Minitest::Test assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, query) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = Base64.encode64(signature).gsub(/\n/, "") # Re-create the Logoutresponse based on these modified parameters, # and ask it to validate the signature. Provide the altered parameter diff --git a/test/metadata_test.rb b/test/metadata_test.rb index b2cdae51..9d8489ed 100644 --- a/test/metadata_test.rb +++ b/test/metadata_test.rb @@ -217,17 +217,42 @@ class MetadataTest < Minitest::Test it "generates Service Provider Metadata with X509Certificate for encrypt" do assert_equal 4, key_descriptors.length assert_equal "signing", key_descriptors[0].attribute("use").value - assert_equal "encryption", key_descriptors[1].attribute("use").value - assert_equal "signing", key_descriptors[2].attribute("use").value + assert_equal "signing", key_descriptors[1].attribute("use").value + assert_equal "encryption", key_descriptors[2].attribute("use").value assert_equal "encryption", key_descriptors[3].attribute("use").value assert_equal 4, cert_nodes.length - assert_equal cert_nodes[0].text, cert_nodes[1].text - assert_equal cert_nodes[2].text, cert_nodes[3].text + assert_equal cert_nodes[0].text, cert_nodes[2].text + assert_equal cert_nodes[1].text, cert_nodes[3].text assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") end end + describe "with check_sp_cert_expiration and expired keys" do + before do + settings.security[:want_assertions_encrypted] = true + settings.security[:check_sp_cert_expiration] = true + valid_pair = CertificateHelper.generate_pair_hash + early_pair = CertificateHelper.generate_pair_hash(not_before: Time.now + 60) + expired_pair = CertificateHelper.generate_pair_hash(not_after: Time.now - 60) + settings.certificate = nil + settings.certificate_new = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [valid_pair, early_pair, expired_pair], + encryption: [valid_pair, early_pair, expired_pair] + } + end + + it "generates Service Provider Metadata with X509Certificate for encrypt" do + assert_equal 2, key_descriptors.length + assert_equal "signing", key_descriptors[0].attribute("use").value + assert_equal "encryption", key_descriptors[1].attribute("use").value + + assert_equal 2, cert_nodes.length + assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") + end + end end describe "when attribute service is configured with multiple attribute values" do diff --git a/test/request_test.rb b/test/request_test.rb index ed3c8ab6..76e8a848 100644 --- a/test/request_test.rb +++ b/test/request_test.rb @@ -268,6 +268,49 @@ class RequestTest < Minitest::Test assert_match %r[], request_xml assert_match %r[], request_xml end + + it "creates a signed request using the first certificate and key" do + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Authrequest.new.create_params(settings) + + request_xml = Base64.decode64(params["SAMLRequest"]) + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match %r[], request_xml + end + + it "creates a signed request using the first valid certificate and key when :check_sp_cert_expiration is true" do + settings.certificate = nil + settings.private_key = nil + settings.security[:check_sp_cert_expiration] = true + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Authrequest.new.create_params(settings) + + request_xml = Base64.decode64(params["SAMLRequest"]) + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match %r[], request_xml + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::Authrequest.new.create_params(settings) + end + end end describe "#create_params signing with HTTP-Redirect binding" do @@ -298,7 +341,6 @@ class RequestTest < Minitest::Test signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg']) assert_equal signature_algorithm, OpenSSL::Digest::SHA1 - assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) end @@ -317,6 +359,41 @@ class RequestTest < Minitest::Test assert_equal signature_algorithm, OpenSSL::Digest::SHA256 assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) end + + it "create a signature parameter using the first certificate and key" do + settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 + settings.compress_request = false + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['SAMLRequest'] + assert params[:RelayState] + assert params['Signature'] + assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1 + + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + + signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA1 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + end + end end it "create the saml:AuthnContextClassRef element correctly" do diff --git a/test/response_test.rb b/test/response_test.rb index aaa55a79..f2fca3eb 100644 --- a/test/response_test.rb +++ b/test/response_test.rb @@ -1374,7 +1374,7 @@ def generate_audience_error(expected, actual) end describe '#sign_document' do - it 'Sign an unsigned SAML Response XML and initiate the SAML object with it' do + it 'sign an unsigned SAML Response XML and initiate the SAML object with it' do xml = Base64.decode64(fixture("test_sign.xml")) document = XMLSecurity::Document.new(xml) @@ -1404,11 +1404,9 @@ def generate_audience_error(expected, actual) @no_signed_assertion = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings) end - it 'returns false if :want_assertion_signed enabled and Assertion not signed' do assert !@no_signed_assertion.send(:validate_signed_elements) assert_includes @no_signed_assertion.errors, "The Assertion of the Response is not signed and the SP requires it" - end it 'returns true if :want_assertion_signed enabled and Assertion is signed' do @@ -1568,6 +1566,14 @@ def generate_audience_error(expected, actual) end end + it "is not possible to decrypt the assertion if private key has expired and :check_sp_expiration is true" do + settings.certificate = ruby_saml_cert_text + settings.security[:check_sp_cert_expiration] = true + assert_raises(OneLogin::RubySaml::ValidationError, "The SP certificate expired.") do + OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + end + end + it "is possible to decrypt the assertion if private key" do response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) @@ -1587,6 +1593,26 @@ def generate_audience_error(expected, actual) assert decrypted.name, "Assertion" end + it "is possible to decrypt the assertion with one invalid and one valid private key" do + settings.private_key = nil + settings.sp_cert_multi = { + encryption: [ + CertificateHelper.generate_pair_hash, + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text } + ] + } + response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + + encrypted_assertion_node = REXML::XPath.first( + response.document, + "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", + { "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" } + ) + decrypted = response.send(:decrypt_assertion, encrypted_assertion_node) + + assert decrypted.name, "Assertion" + end + it "is possible to decrypt the assertion if private key provided and EncryptedKey RetrievalMethod presents in response" do settings.private_key = ruby_saml_key_text resp = read_response('response_with_retrieval_method.xml') diff --git a/test/settings_test.rb b/test/settings_test.rb index 6d14d251..8e7cb9f6 100644 --- a/test/settings_test.rb +++ b/test/settings_test.rb @@ -18,7 +18,7 @@ class SettingsTest < Minitest::Test :sp_name_qualifier, :name_identifier_format, :name_identifier_value, :name_identifier_value_requested, :sessionindex, :attributes_index, :passive, :force_authn, :compress_request, :double_quote_xml_attribute_values, :message_max_bytesize, - :security, :certificate, :private_key, + :security, :certificate, :private_key, :certificate_new, :sp_cert_multi, :authn_context, :authn_context_comparison, :authn_context_decl_ref, :assertion_consumer_logout_service_url ] @@ -382,7 +382,6 @@ class SettingsTest < Minitest::Test @settings.get_sp_cert_new } end - end describe "#get_sp_key" do @@ -408,7 +407,6 @@ class SettingsTest < Minitest::Test @settings.get_sp_key } end - end describe "#get_fingerprint" do @@ -440,5 +438,302 @@ class SettingsTest < Minitest::Test assert fingerprint.downcase == ruby_saml_cert_fingerprint.downcase end end + + describe "#get_sp_certs (base cases)" do + let(:cert_text1) { ruby_saml_cert_text } + let(:cert_text2) { ruby_saml_cert2.to_pem } + let(:cert_text3) { CertificateHelper.generate_cert.to_pem } + let(:key_text1) { ruby_saml_key_text } + let(:key_text2) { CertificateHelper.generate_key.to_pem } + + it "returns certs for single case" do + @settings.certificate = cert_text1 + @settings.private_key = key_text1 + + actual = @settings.get_sp_certs + expected = [[cert_text1, key_text1]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "returns certs for single case with new cert" do + @settings.certificate = cert_text1 + @settings.certificate_new = cert_text2 + @settings.private_key = key_text1 + + actual = @settings.get_sp_certs + expected = [[cert_text1, key_text1], [cert_text2, key_text1]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "returns certs for multi case" do + @settings.sp_cert_multi = { + signing: [{ certificate: cert_text1, private_key: key_text1 }, + { certificate: cert_text2, private_key: key_text1 }], + encryption: [{ certificate: cert_text2, private_key: key_text1 }, + { certificate: cert_text3, private_key: key_text2 }] + } + + actual = @settings.get_sp_certs + expected_signing = [[cert_text1, key_text1], [cert_text2, key_text1]] + expected_encryption = [[cert_text2, key_text1], [cert_text3, key_text2]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected_signing, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected_encryption, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "sp_cert_multi allows sending only signing" do + @settings.sp_cert_multi = { + signing: [{ certificate: cert_text1, private_key: key_text1 }, + { certificate: cert_text2, private_key: key_text1 }] + } + + actual = @settings.get_sp_certs + expected_signing = [[cert_text1, key_text1], [cert_text2, key_text1]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected_signing, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal [], actual[:encryption] + end + + it "raises error when sp_cert_multi is not a Hash" do + @settings.sp_cert_multi = 'invalid_type' + + error_message = 'sp_cert_multi must be a Hash' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "raises error when sp_cert_multi does not contain an Array of Hashes" do + @settings.sp_cert_multi = { signing: 'invalid_type' } + + error_message = 'sp_cert_multi :signing node must be an Array of Hashes' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "raises error when sp_cert_multi inner node missing :certificate" do + @settings.sp_cert_multi = { signing: [{ private_key: key_text1 }] } + + error_message = 'sp_cert_multi :signing node Hashes must specify keys :certificate and :private_key' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "raises error when sp_cert_multi inner node missing :private_key" do + @settings.sp_cert_multi = { signing: [{ certificate: cert_text1 }] } + + error_message = 'sp_cert_multi :signing node Hashes must specify keys :certificate and :private_key' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "handles sp_cert_multi with string keys" do + @settings.sp_cert_multi = { + 'signing' => [{ 'certificate' => cert_text1, 'private_key' => key_text1 }], + 'encryption' => [{ 'certificate' => cert_text2, 'private_key' => key_text1 }] + } + + actual = @settings.get_sp_certs + expected_signing = [[cert_text1, key_text1]] + expected_encryption = [[cert_text2, key_text1]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected_signing, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected_encryption, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "handles sp_cert_multi with alternate inner keys :cert and :key" do + @settings.sp_cert_multi = { + signing: [{ cert: cert_text1, key: key_text1 }], + encryption: [{ 'cert' => cert_text2, 'key' => key_text1 }] + } + + actual = @settings.get_sp_certs + expected_signing = [[cert_text1, key_text1]] + expected_encryption = [[cert_text2, key_text1]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected_signing, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected_encryption, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "raises error when both sp_cert_multi and certificate are specified" do + @settings.sp_cert_multi = { signing: [{ certificate: cert_text1, private_key: key_text1 }] } + @settings.certificate = cert_text1 + + error_message = 'Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "raises error when both sp_cert_multi and certificate_new are specified" do + @settings.sp_cert_multi = { signing: [{ certificate: cert_text1, private_key: key_text1 }] } + @settings.certificate_new = cert_text2 + + error_message = 'Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "raises error when both sp_cert_multi and private_key are specified" do + @settings.sp_cert_multi = { signing: [{ certificate: cert_text1, private_key: key_text1 }] } + @settings.private_key = key_text1 + + error_message = 'Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + end + + describe "#get_sp_certs" do + let(:valid_pair) { CertificateHelper.generate_pair_hash } + let(:early_pair) { CertificateHelper.generate_pair_hash(not_before: Time.now + 60) } + let(:expired_pair) { CertificateHelper.generate_pair_hash(not_after: Time.now - 60) } + + it "returns all certs when check_sp_cert_expiration is false" do + @settings.security = { check_sp_cert_expiration: false } + @settings.sp_cert_multi = { signing: [valid_pair, expired_pair], encryption: [valid_pair, early_pair] } + + actual = @settings.get_sp_certs + expected_signing = [valid_pair, expired_pair].map(&:values) + expected_encryption = [valid_pair, early_pair].map(&:values) + assert_equal expected_signing, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected_encryption, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "returns only active certs when check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { signing: [valid_pair, expired_pair], encryption: [valid_pair, early_pair] } + + actual = @settings.get_sp_certs + expected_active = [valid_pair].map(&:values) + assert_equal expected_active, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected_active, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "raises error when all certificates are expired in signing and check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { signing: [expired_pair], encryption: [valid_pair] } + + assert_raises OneLogin::RubySaml::ValidationError do + @settings.get_sp_certs + end + end + + it "raises error when all certificates are expired in encryption and check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { signing: [valid_pair], encryption: [expired_pair] } + + assert_raises OneLogin::RubySaml::ValidationError do + @settings.get_sp_certs + end + end + + it "returns empty arrays for signing and encryption if no pairs are present" do + @settings.sp_cert_multi = { signing: [], encryption: [] } + + actual = @settings.get_sp_certs + assert_empty actual[:signing] + assert_empty actual[:encryption] + end + end + + describe "#get_sp_signing_pair and #get_sp_signing_key" do + let(:valid_pair) { CertificateHelper.generate_pair_hash } + let(:early_pair) { CertificateHelper.generate_pair_hash(not_before: Time.now + 60) } + let(:expired) { CertificateHelper.generate_pair_hash(not_after: Time.now - 60) } + + it "returns nil when no signing pairs are present" do + @settings.sp_cert_multi = { signing: [] } + + assert_nil @settings.get_sp_signing_pair + assert_nil @settings.get_sp_signing_key + end + + it "returns the first pair if check_sp_cert_expiration is false" do + @settings.security = { check_sp_cert_expiration: false } + @settings.sp_cert_multi = { signing: [early_pair, expired, valid_pair] } + + assert_equal early_pair.values, @settings.get_sp_signing_pair.map(&:to_pem) + assert_equal early_pair[:private_key], @settings.get_sp_signing_key.to_pem + end + + it "returns the first active pair when check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { signing: [early_pair, expired, valid_pair] } + + assert_equal valid_pair.values, @settings.get_sp_signing_pair.map(&:to_pem) + assert_equal valid_pair[:private_key], @settings.get_sp_signing_key.to_pem + end + + it "raises error when all certificates are expired and check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { signing: [early_pair, expired] } + + assert_raises OneLogin::RubySaml::ValidationError do + @settings.get_sp_signing_pair + end + + assert_raises OneLogin::RubySaml::ValidationError do + @settings.get_sp_signing_key + end + end + end + + describe "#get_sp_decryption_keys" do + let(:valid_pair) { CertificateHelper.generate_pair_hash } + let(:early_pair) { CertificateHelper.generate_pair_hash(not_before: Time.now + 60) } + let(:expired_pair) { CertificateHelper.generate_pair_hash(not_after: Time.now - 60) } + + it "returns an empty array when no decryption pairs are present" do + @settings.sp_cert_multi = { encryption: [] } + + assert_empty @settings.get_sp_decryption_keys + end + + it "returns all keys when check_sp_cert_expiration is false" do + @settings.security = { check_sp_cert_expiration: false } + @settings.sp_cert_multi = { encryption: [early_pair, expired_pair, valid_pair] } + + expected_keys = [early_pair, expired_pair, valid_pair].map { |pair| pair[:private_key] } + actual_keys = @settings.get_sp_decryption_keys.map(&:to_pem) + assert_equal expected_keys, actual_keys + end + + it "returns only keys of active certificates when check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { encryption: [early_pair, expired_pair, valid_pair] } + + expected_keys = [valid_pair[:private_key]] + actual_keys = @settings.get_sp_decryption_keys.map(&:to_pem) + assert_equal expected_keys, actual_keys + end + + it "raises error when all certificates are expired and check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { encryption: [early_pair, expired_pair] } + + assert_raises OneLogin::RubySaml::ValidationError do + @settings.get_sp_decryption_keys + end + end + + it "removes duplicates" do + @settings.sp_cert_multi = { encryption: [early_pair, valid_pair, early_pair, valid_pair] } + + expected_keys = [early_pair, valid_pair].map { |pair| pair[:private_key] } + actual_keys = @settings.get_sp_decryption_keys.map(&:to_pem) + + assert_equal expected_keys, actual_keys + end + end end end diff --git a/test/slo_logoutrequest_test.rb b/test/slo_logoutrequest_test.rb index f7d6e7db..0415f20f 100644 --- a/test/slo_logoutrequest_test.rb +++ b/test/slo_logoutrequest_test.rb @@ -281,11 +281,13 @@ class RubySamlTest < Minitest::Test logout_request.settings.idp_entity_id = 'https://app.onelogin.com/saml/metadata/SOMEACCOUNT' assert logout_request.send(:validate_issuer) end + it "return false when the issuer of the Logout Request does not match the IdP entityId" do logout_request.settings.idp_entity_id = 'http://idp.example.com/invalid' assert !logout_request.send(:validate_issuer) assert_includes logout_request.errors, "Doesn't match the issuer, expected: <#{logout_request.settings.idp_entity_id}>, but was: " end + it "raise when the issuer of the Logout Request does not match the IdP entityId" do logout_request.settings.idp_entity_id = 'http://idp.example.com/invalid' logout_request.soft = false @@ -400,7 +402,7 @@ class RubySamlTest < Minitest::Test assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, query) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = Base64.encode64(signature).gsub(/\n/, "") # Construct SloLogoutrequest and ask it to validate the signature. # It will do it incorrectly, because it will compute it based on re-encoded @@ -435,7 +437,7 @@ class RubySamlTest < Minitest::Test assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, query) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = Base64.encode64(signature).gsub(/\n/, "") # Construct SloLogoutrequest and ask it to validate the signature. # Provide the altered parameter in its raw URI-encoded form, @@ -474,7 +476,7 @@ class RubySamlTest < Minitest::Test sign_algorithm = XMLSecurity::BaseDocument.new.algorithm( settings.security[:signature_method] ) - signature = settings.get_sp_key.sign(sign_algorithm.new, query) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = downcased_escape(Base64.encode64(signature).gsub(/\n/, "")) # Then parameters are usually unescaped, like we manage them in rails @@ -552,7 +554,6 @@ class RubySamlTest < Minitest::Test logout_request_sign_test.settings = settings assert !logout_request_sign_test.send(:validate_signature) assert_includes logout_request_sign_test.errors, "Invalid Signature on Logout Request" - end end end diff --git a/test/slo_logoutresponse_test.rb b/test/slo_logoutresponse_test.rb index e79bbe1d..7c9669f0 100644 --- a/test/slo_logoutresponse_test.rb +++ b/test/slo_logoutresponse_test.rb @@ -99,7 +99,6 @@ class SloLogoutresponseTest < Minitest::Test end describe "signing with HTTP-POST binding" do - before do settings.idp_sso_service_binding = :redirect settings.idp_slo_service_binding = :post @@ -143,6 +142,7 @@ class SloLogoutresponseTest < Minitest::Test it "create a signed logout response" do logout_request.settings = settings + params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") response_xml = Base64.decode64(params["SAMLResponse"]) @@ -154,6 +154,7 @@ class SloLogoutresponseTest < Minitest::Test it "create a signed logout response with 256 digest and signature methods" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 settings.security[:digest_method] = XMLSecurity::Document::SHA256 + logout_request.settings = settings params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") @@ -175,6 +176,54 @@ class SloLogoutresponseTest < Minitest::Test assert_match(//, response_xml) assert_match(//, response_xml) end + + it "create a signed logout response using the first certificate and key" do + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + logout_request.settings = settings + + params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + + response_xml = Base64.decode64(params["SAMLResponse"]) + assert_match %r[([a-zA-Z0-9/+=]+)], response_xml + assert_match(//, response_xml) + assert_match(//, response_xml) + end + + it "create a signed logout response using the first valid certificate and key when :check_sp_cert_expiration is true" do + settings.certificate = nil + settings.private_key = nil + settings.security[:check_sp_cert_expiration] = true + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + logout_request.settings = settings + + params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + + response_xml = Base64.decode64(params["SAMLResponse"]) + assert_match %r[([a-zA-Z0-9/+=]+)], response_xml + assert_match(//, response_xml) + assert_match(//, response_xml) + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + logout_request.settings = settings + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + end + end end describe "signing with HTTP-Redirect binding" do @@ -261,10 +310,44 @@ class SloLogoutresponseTest < Minitest::Test assert_equal signature_algorithm, OpenSSL::Digest::SHA512 assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) end + + it "create a signature parameter using the first certificate and key" do + settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 + settings.compress_request = false + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + assert params['SAMLResponse'] + assert params[:RelayState] + assert params['Signature'] + assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1 + + query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + + signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA1 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + end + end end describe "DEPRECATED: signing with HTTP-POST binding via :embed_sign" do - before do settings.compress_response = false settings.security[:logout_responses_signed] = true diff --git a/test/test_helper.rb b/test/test_helper.rb index ae927ae5..1a0427f8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -17,6 +17,7 @@ require 'minitest/autorun' require 'mocha/setup' require 'timecop' +Dir[File.expand_path('../helpers/**/*.rb', __FILE__)].each { |f| require f } Bundler.require :default, :test diff --git a/test/utils_test.rb b/test/utils_test.rb index 36d0bd93..35577191 100644 --- a/test/utils_test.rb +++ b/test/utils_test.rb @@ -88,7 +88,6 @@ def result(duration, reference = 0) invalid_chained_certificate1 = read_certificate("invalid_chained_certificate1") assert_equal formatted_chained_certificate, OneLogin::RubySaml::Utils.format_cert(invalid_chained_certificate1) end - end describe ".format_private_key" do @@ -151,7 +150,49 @@ def result(duration, reference = 0) end end - describe "build_query" do + describe '.build_cert_object' do + it 'returns a certificate object for valid certificate string' do + cert_object = OneLogin::RubySaml::Utils.build_cert_object(ruby_saml_cert_text) + assert_instance_of OpenSSL::X509::Certificate, cert_object + end + + it 'returns nil for nil certificate string' do + assert_nil OneLogin::RubySaml::Utils.build_cert_object(nil) + end + + it 'returns nil for empty certificate string' do + assert_nil OneLogin::RubySaml::Utils.build_cert_object('') + end + + it 'raises error when given an invalid certificate string' do + assert_raises OpenSSL::X509::CertificateError do + OneLogin::RubySaml::Utils.build_cert_object('Foobar') + end + end + end + + describe '.build_private_key_object' do + it 'returns a private key object for valid private key string' do + private_key_object = OneLogin::RubySaml::Utils.build_private_key_object(ruby_saml_key_text) + assert_instance_of OpenSSL::PKey::RSA, private_key_object + end + + it 'returns nil for nil private key string' do + assert_nil OneLogin::RubySaml::Utils.build_private_key_object(nil) + end + + it 'returns nil for empty private key string' do + assert_nil OneLogin::RubySaml::Utils.build_private_key_object('') + end + + it 'raises error when given an invalid private key string' do + assert_raises OpenSSL::PKey::RSAError do + OneLogin::RubySaml::Utils.build_private_key_object('Foobar') + end + end + end + + describe ".build_query" do it "returns the query string" do params = {} params[:type] = "SAMLRequest" @@ -163,7 +204,7 @@ def result(duration, reference = 0) end end - describe "#verify_signature" do + describe ".verify_signature" do before do @params = {} @params[:cert] = ruby_saml_cert @@ -182,7 +223,7 @@ def result(duration, reference = 0) end end - describe "#status_error_msg" do + describe ".status_error_msg" do it "returns a error msg with status_code and status message" do error_msg = "The status code of the Logout Response was not Success" status_code = "urn:oasis:names:tc:SAML:2.0:status:Requester" @@ -217,7 +258,7 @@ def result(duration, reference = 0) end end - describe 'uri_match' do + describe '.uri_match?' do it 'matches two urls' do destination = 'http://www.example.com/test?var=stuff' settings = 'http://www.example.com/test?var=stuff' @@ -267,7 +308,7 @@ def result(duration, reference = 0) end end - describe 'element_text' do + describe '.element_text' do it 'returns the element text' do element = REXML::Document.new('element text').elements.first assert_equal 'element text', OneLogin::RubySaml::Utils.element_text(element) @@ -301,7 +342,117 @@ def result(duration, reference = 0) element = REXML::Document.new('').elements.first assert_equal '', OneLogin::RubySaml::Utils.element_text(element) end + end + end + + describe '.decrypt_multi' do + let(:private_key) { ruby_saml_key } + let(:invalid_key1) { CertificateHelper.generate_key } + let(:invalid_key2) { CertificateHelper.generate_key } + let(:settings) { OneLogin::RubySaml::Settings.new(:private_key => private_key.to_pem) } + let(:response) { OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) } + let(:encrypted) do + REXML::XPath.first( + response.document, + "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", + { "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" } + ) + end + + it 'successfully decrypts with the first private key' do + assert_match /\A