diff --git a/lib/ruby_saml/authrequest.rb b/lib/ruby_saml/authrequest.rb index 95a4433a..6aeccc10 100644 --- a/lib/ruby_saml/authrequest.rb +++ b/lib/ruby_saml/authrequest.rb @@ -77,14 +77,14 @@ def create_params(settings, params={}) sp_signing_key = settings.get_sp_signing_key if binding_redirect && settings.security[:authn_requests_signed] && sp_signing_key - params['SigAlg'] = settings.security[:signature_method] + params['SigAlg'] = settings.get_sp_signature_method url_string = RubySaml::Utils.build_query( type: 'SAMLRequest', data: base64_request, relay_state: relay_state, sig_alg: params['SigAlg'] ) - sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm(settings.security[:signature_method]) + sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method) signature = sp_signing_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end @@ -185,7 +185,7 @@ def create_xml_document(settings) def sign_document(document, settings) 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]) + document.sign_document(private_key, cert, settings.get_sp_signature_method, settings.get_sp_digest_method) end document diff --git a/lib/ruby_saml/idp_metadata_parser.rb b/lib/ruby_saml/idp_metadata_parser.rb index 7bf1cf93..f54314fb 100644 --- a/lib/ruby_saml/idp_metadata_parser.rb +++ b/lib/ruby_saml/idp_metadata_parser.rb @@ -398,7 +398,7 @@ def fingerprint(certificate, fingerprint_algorithm = RubySaml::XML::Document::SH cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate)) - fingerprint_alg = RubySaml::XML::BaseDocument.new.algorithm(fingerprint_algorithm).new + fingerprint_alg = RubySaml::XML::Crypto.hash_algorithm(fingerprint_algorithm).new fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":") end end diff --git a/lib/ruby_saml/logoutrequest.rb b/lib/ruby_saml/logoutrequest.rb index a82b9393..9ebf325c 100644 --- a/lib/ruby_saml/logoutrequest.rb +++ b/lib/ruby_saml/logoutrequest.rb @@ -75,14 +75,14 @@ def create_params(settings, params={}) sp_signing_key = settings.get_sp_signing_key if binding_redirect && settings.security[:logout_requests_signed] && sp_signing_key - params['SigAlg'] = settings.security[:signature_method] + params['SigAlg'] = settings.get_sp_signature_method url_string = RubySaml::Utils.build_query( type: 'SAMLRequest', data: base64_request, relay_state: relay_state, sig_alg: params['SigAlg'] ) - sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm(settings.security[:signature_method]) + sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method) signature = settings.get_sp_signing_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end @@ -144,7 +144,7 @@ def sign_document(document, settings) # embed signature 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]) + document.sign_document(private_key, cert, settings.get_sp_signature_method, settings.get_sp_digest_method) end document diff --git a/lib/ruby_saml/metadata.rb b/lib/ruby_saml/metadata.rb index b76624d1..a39850d7 100644 --- a/lib/ruby_saml/metadata.rb +++ b/lib/ruby_saml/metadata.rb @@ -142,7 +142,7 @@ def embed_signature(meta_doc, settings) 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]) + meta_doc.sign_document(private_key, cert, settings.get_sp_signature_method, settings.get_sp_digest_method) end def output_xml(meta_doc, pretty_print) diff --git a/lib/ruby_saml/settings.rb b/lib/ruby_saml/settings.rb index 36badd69..54e3fb18 100644 --- a/lib/ruby_saml/settings.rb +++ b/lib/ruby_saml/settings.rb @@ -126,7 +126,7 @@ def get_fingerprint idp_cert_fingerprint || begin idp_cert = get_idp_cert if idp_cert - fingerprint_alg = RubySaml::XML::BaseDocument.new.algorithm(idp_cert_fingerprint_algorithm).new + fingerprint_alg = RubySaml::XML::Crypto.hash_algorithm(idp_cert_fingerprint_algorithm).new fingerprint_alg.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":") end end @@ -159,7 +159,7 @@ def get_idp_cert_multi certs end - # @return [Hash>>] + # @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. @@ -179,7 +179,7 @@ def get_sp_certs active_certs.freeze end - # @return [Array] + # @return [Array] # The SP signing certificate and private key. def get_sp_signing_pair get_sp_certs[:signing].first @@ -267,6 +267,44 @@ def get_binding(value) end end + # @return [String] The XML Signature Algorithm attribute. + # + # This method is intentionally hacky for backwards compatibility of the + # settings.security[:signature_method] parameter. Previously, this parameter + # could have a value such as "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + # which assumes the public key type RSA. To add support for DSA and ECDSA, we will now + # ignore the "rsa-" prefix and only use the "sha256" hash algorithm component. + def get_sp_signature_method + sig_alg = security[:signature_method] || 'sha1' # TODO: change to sha256 by default + hash_alg = sig_alg.to_s.match(/(?:\A|[#_-])(sha\d+)\z/i)[1] + key_alg = case get_sp_signing_key + when OpenSSL::PKey::RSA then 'RSA' + when OpenSSL::PKey::DSA then 'DSA' + when OpenSSL::PKey::EC then 'ECDSA' + else # rubocop:disable Lint/DuplicateBranch + # raise ArgumentError.new("Unsupported signing key type: #{get_sp_signing_key.class}") + 'RSA' + end + + begin + RubySaml::XML::Crypto.const_get("#{key_alg}_#{hash_alg}".upcase) + rescue NameError + raise ArgumentError.new("Unsupported signature method: #{sig_alg}") + end + end + + # @return [String] The XML Signature Digest attribute. + def get_sp_digest_method + digest_alg = security[:digest_method] || 'sha1' # TODO: change to sha256 by default + alg = digest_alg.to_s.match(/(?:\A|#)(sha\d+)\z/i)[1] + + begin + RubySaml::XML::Crypto.const_get(alg.upcase) + rescue NameError + raise ArgumentError.new("Unsupported signature method: #{digest_alg}") + end + end + # @deprecated Will be removed in v2.1.0 def certificate_new certificate_new_deprecation diff --git a/lib/ruby_saml/slo_logoutresponse.rb b/lib/ruby_saml/slo_logoutresponse.rb index b17d81f7..3cd5a215 100644 --- a/lib/ruby_saml/slo_logoutresponse.rb +++ b/lib/ruby_saml/slo_logoutresponse.rb @@ -84,14 +84,14 @@ def create_params(settings, request_id = nil, logout_message = nil, params = {}, sp_signing_key = settings.get_sp_signing_key if binding_redirect && settings.security[:logout_responses_signed] && sp_signing_key - params['SigAlg'] = settings.security[:signature_method] + params['SigAlg'] = settings.get_sp_signature_method url_string = RubySaml::Utils.build_query( type: 'SAMLResponse', data: base64_response, relay_state: relay_state, sig_alg: params['SigAlg'] ) - sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm(settings.security[:signature_method]) + sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method) signature = sp_signing_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end @@ -155,7 +155,7 @@ def sign_document(document, settings) # embed signature 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]) + document.sign_document(private_key, cert, settings.get_sp_signature_method, settings.get_sp_digest_method) end document diff --git a/lib/ruby_saml/utils.rb b/lib/ruby_saml/utils.rb index e055925e..903ae8c8 100644 --- a/lib/ruby_saml/utils.rb +++ b/lib/ruby_saml/utils.rb @@ -124,14 +124,25 @@ def build_cert_object(pem) OpenSSL::X509::Certificate.new(pem) end - # Given a private key string, return an OpenSSL::PKey::RSA object. + # Given a private key string, return an OpenSSL::PKey::PKey object. # # @param pem [String] The original private key. - # @return [OpenSSL::PKey::RSA] The private key object. + # @return [OpenSSL::PKey::PKey] The private key object. def build_private_key_object(pem) return unless (pem = PemFormatter.format_private_key(pem, multi: false)) - OpenSSL::PKey::RSA.new(pem) + error = nil + [OpenSSL::PKey::RSA, + OpenSSL::PKey::DSA, + OpenSSL::PKey::EC].each do |key_class| + begin + return key_class.new(pem) + rescue OpenSSL::PKey::PKeyError => e + error ||= e + end + end + + raise error end # Build the Query String signature that will be used in the HTTP-Redirect binding @@ -213,7 +224,7 @@ def escape_request_param(param, lowercase_url_encoding) # def verify_signature(params) cert, sig_alg, signature, query_string = %i[cert sig_alg signature query_string].map { |k| params[k]} - signature_algorithm = RubySaml::XML::BaseDocument.new.algorithm(sig_alg) + signature_algorithm = RubySaml::XML::Crypto.hash_algorithm(sig_alg) cert.public_key.verify(signature_algorithm.new, Base64.decode64(signature), query_string) end @@ -243,7 +254,7 @@ def status_error_msg(error_msg, raw_status_code = nil, status_message = nil) # 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 + # @param private_keys [Array] The Service provider private key # @return [String] The decrypted data def decrypt_multi(encrypted_node, private_keys) raise ArgumentError.new('private_keys must be specified') if !private_keys || private_keys.empty? @@ -260,7 +271,7 @@ def decrypt_multi(encrypted_node, private_keys) # 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 private_key [OpenSSL::PKey::PKey] The Service provider private key # @return [String] The decrypted data def decrypt_data(encrypted_node, private_key) encrypt_data = REXML::XPath.first( @@ -286,7 +297,7 @@ def decrypt_data(encrypted_node, private_key) # Obtains the symmetric key from the EncryptedData element # @param encrypt_data [REXML::Element] The EncryptedData element - # @param private_key [OpenSSL::PKey::RSA] The Service provider private key + # @param private_key [OpenSSL::PKey::PKey] The Service provider private key # @return [String] The symmetric key def retrieve_symmetric_key(encrypt_data, private_key) encrypted_key = REXML::XPath.first( diff --git a/lib/ruby_saml/xml.rb b/lib/ruby_saml/xml.rb index b6ab1b62..2a85a665 100644 --- a/lib/ruby_saml/xml.rb +++ b/lib/ruby_saml/xml.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'ruby_saml/xml/crypto' require 'ruby_saml/xml/base_document' require 'ruby_saml/xml/document' require 'ruby_saml/xml/signed_document' diff --git a/lib/ruby_saml/xml/base_document.rb b/lib/ruby_saml/xml/base_document.rb index 8cfcce26..bb6a122b 100644 --- a/lib/ruby_saml/xml/base_document.rb +++ b/lib/ruby_saml/xml/base_document.rb @@ -1,55 +1,35 @@ # frozen_string_literal: true require 'rexml/document' +require 'rexml/security' require 'rexml/xpath' require 'nokogiri' require 'openssl' require 'digest/sha1' require 'digest/sha2' +require 'ruby_saml/xml/crypto' module RubySaml module XML class BaseDocument < REXML::Document + # TODO: This affects the global state REXML::Security.entity_expansion_limit = 0 - C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#' - DSIG = 'http://www.w3.org/2000/09/xmldsig#' + # @deprecated Constants moved to Crypto module + C14N = RubySaml::XML::Crypto::C14N + DSIG = RubySaml::XML::Crypto::DSIG + NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT | Nokogiri::XML::ParseOptions::NONET - def canon_algorithm(element) - algorithm = element - if algorithm.is_a?(REXML::Element) - algorithm = element.attribute('Algorithm').value - end - - case algorithm - when 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315', - 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments' - Nokogiri::XML::XML_C14N_1_0 - when 'http://www.w3.org/2006/12/xml-c14n11', - 'http://www.w3.org/2006/12/xml-c14n11#WithComments' - Nokogiri::XML::XML_C14N_1_1 - else - Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0 - end + # @deprecated Remove in v2.1.0 + def canon_algorithm(algorithm) + RubySaml::XML::Crypto.canon_algorithm(algorithm) end - def algorithm(element) - algorithm = element - if algorithm.is_a?(REXML::Element) - algorithm = element.attribute('Algorithm').value - end - - algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && ::Regexp.last_match(2).to_i - - case algorithm - when 1 then OpenSSL::Digest::SHA1 - when 384 then OpenSSL::Digest::SHA384 - when 512 then OpenSSL::Digest::SHA512 - else - OpenSSL::Digest::SHA256 - end + # @deprecated Remove in v2.1.0 + def algorithm(algorithm) + RubySaml::XML::Crypto.hash_algorithm(algorithm) end end end diff --git a/lib/ruby_saml/xml/crypto.rb b/lib/ruby_saml/xml/crypto.rb new file mode 100644 index 00000000..e06515bb --- /dev/null +++ b/lib/ruby_saml/xml/crypto.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'rexml/element' +require 'openssl' +require 'nokogiri' +require 'digest/sha1' +require 'digest/sha2' + +module RubySaml + module XML + # XML Signature and Canonicalization algorithms + module Crypto + extend self + + C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#' + DSIG = 'http://www.w3.org/2000/09/xmldsig#' + RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + RSA_SHA224 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha224' + RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' + RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' + DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' + DSA_SHA256 = 'http://www.w3.org/2009/xmldsig11#dsa-sha256' + ECDSA_SHA1 = 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1' + ECDSA_SHA224 = 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224' + ECDSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256' + ECDSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384' + ECDSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512' + SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1' + SHA224 = 'http://www.w3.org/2001/04/xmldsig-more#sha224' + SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256' + SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384' + SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512' + ENVELOPED_SIG = 'http://www.w3.org/2000/09/xmldsig#enveloped-signature' + + def canon_algorithm(element, default: true) + case get_algorithm_attr(element) + when %r{\Ahttp://www\.w3\.org/TR/2001/REC-xml-c14n-20010315#?(?:WithComments)?\z}i + Nokogiri::XML::XML_C14N_1_0 + when %r{\Ahttp://www\.w3\.org/2006/12/xml-c14n11#?(?:WithComments)?\z}i + Nokogiri::XML::XML_C14N_1_1 + when %r{\Ahttp://www\.w3\.org/2001/10/xml-exc-c14n#?(?:WithComments)?\z}i + Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0 + else + Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0 if default + end + end + + def signature_algorithm(element) + alg = get_algorithm_attr(element) + match_data = alg&.downcase&.match(/(?:\A|#)(rsa|dsa|ecdsa)-(sha\d+)\z/i) || {} + key_alg = match_data[1] + hash_alg = match_data[2] + + key = case key_alg + when 'rsa' then OpenSSL::PKey::RSA + when 'dsa' then OpenSSL::PKey::DSA + when 'ecdsa' then OpenSSL::PKey::EC + else # rubocop:disable Lint/DuplicateBranch + # TODO: raise ArgumentError.new("Invalid key algorithm: #{alg}") + OpenSSL::PKey::RSA + end + + [key, hash_algorithm(hash_alg)] + end + + def hash_algorithm(element) + alg = get_algorithm_attr(element) + hash_alg = alg&.downcase&.match(/(?:\A|[#-])(sha\d+)\z/i)&.[](1) + + case hash_alg + when 'sha1' then OpenSSL::Digest::SHA1 + when 'sha224' then OpenSSL::Digest::SHA224 + when 'sha256' then OpenSSL::Digest::SHA256 + when 'sha384' then OpenSSL::Digest::SHA384 + when 'sha512' then OpenSSL::Digest::SHA512 + else # rubocop:disable Lint/DuplicateBranch + # TODO: raise ArgumentError.new("Invalid hash algorithm: #{alg}") + OpenSSL::Digest::SHA256 + end + end + + private + + def get_algorithm_attr(element) + if element.is_a?(REXML::Element) + element.attribute('Algorithm').value + elsif element + element + end + end + end + end +end diff --git a/lib/ruby_saml/xml/document.rb b/lib/ruby_saml/xml/document.rb index a37ab09f..4a543801 100644 --- a/lib/ruby_saml/xml/document.rb +++ b/lib/ruby_saml/xml/document.rb @@ -5,17 +5,28 @@ module RubySaml module XML class Document < BaseDocument - RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' - RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' - RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' - RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' - SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1' - SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256' - SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384' - SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512' - ENVELOPED_SIG = 'http://www.w3.org/2000/09/xmldsig#enveloped-signature' INC_PREFIX_LIST = '#default samlp saml ds xs xsi md' + # @deprecated Constants moved to Crypto module + RSA_SHA1 = RubySaml::XML::Crypto::RSA_SHA1 + RSA_SHA224 = RubySaml::XML::Crypto::RSA_SHA224 + RSA_SHA256 = RubySaml::XML::Crypto::RSA_SHA256 + RSA_SHA384 = RubySaml::XML::Crypto::RSA_SHA384 + RSA_SHA512 = RubySaml::XML::Crypto::RSA_SHA512 + DSA_SHA1 = RubySaml::XML::Crypto::DSA_SHA1 + DSA_SHA256 = RubySaml::XML::Crypto::DSA_SHA256 + ECDSA_SHA1 = RubySaml::XML::Crypto::ECDSA_SHA1 + ECDSA_SHA224 = RubySaml::XML::Crypto::ECDSA_SHA224 + ECDSA_SHA256 = RubySaml::XML::Crypto::ECDSA_SHA256 + ECDSA_SHA384 = RubySaml::XML::Crypto::ECDSA_SHA384 + ECDSA_SHA512 = RubySaml::XML::Crypto::ECDSA_SHA512 + SHA1 = RubySaml::XML::Crypto::SHA1 + SHA224 = RubySaml::XML::Crypto::SHA224 + SHA256 = RubySaml::XML::Crypto::SHA256 + SHA384 = RubySaml::XML::Crypto::SHA384 + SHA512 = RubySaml::XML::Crypto::SHA512 + ENVELOPED_SIG = RubySaml::XML::Crypto::ENVELOPED_SIG + attr_writer :uuid def uuid @@ -42,9 +53,9 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA256, diges config.options = RubySaml::XML::BaseDocument::NOKOGIRI_OPTIONS end - signature_element = REXML::Element.new('ds:Signature').add_namespace('ds', DSIG) + signature_element = REXML::Element.new('ds:Signature').add_namespace('ds', RubySaml::XML::Crypto::DSIG) signed_info_element = signature_element.add_element('ds:SignedInfo') - signed_info_element.add_element('ds:CanonicalizationMethod', {'Algorithm' => C14N}) + signed_info_element.add_element('ds:CanonicalizationMethod', {'Algorithm' => RubySaml::XML::Crypto::C14N}) signed_info_element.add_element('ds:SignatureMethod', {'Algorithm'=>signature_method}) # Add Reference @@ -52,30 +63,30 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA256, diges # Add Transforms transforms_element = reference_element.add_element('ds:Transforms') - transforms_element.add_element('ds:Transform', {'Algorithm' => ENVELOPED_SIG}) - c14element = transforms_element.add_element('ds:Transform', {'Algorithm' => C14N}) - c14element.add_element('ec:InclusiveNamespaces', {'xmlns:ec' => C14N, 'PrefixList' => INC_PREFIX_LIST}) + transforms_element.add_element('ds:Transform', {'Algorithm' => RubySaml::XML::Crypto::ENVELOPED_SIG}) + c14element = transforms_element.add_element('ds:Transform', {'Algorithm' => RubySaml::XML::Crypto::C14N}) + c14element.add_element('ec:InclusiveNamespaces', {'xmlns:ec' => RubySaml::XML::Crypto::C14N, 'PrefixList' => INC_PREFIX_LIST}) digest_method_element = reference_element.add_element('ds:DigestMethod', {'Algorithm' => digest_method}) inclusive_namespaces = INC_PREFIX_LIST.split - canon_doc = noko.canonicalize(canon_algorithm(C14N), inclusive_namespaces) - reference_element.add_element('ds:DigestValue').text = compute_digest(canon_doc, algorithm(digest_method_element)) + canon_doc = noko.canonicalize(RubySaml::XML::Crypto.canon_algorithm(RubySaml::XML::Crypto::C14N), inclusive_namespaces) + reference_element.add_element('ds:DigestValue').text = compute_digest(canon_doc, RubySaml::XML::Crypto.hash_algorithm(digest_method_element)) # add SignatureValue noko_sig_element = Nokogiri::XML(signature_element.to_s) do |config| config.options = RubySaml::XML::BaseDocument::NOKOGIRI_OPTIONS end - noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG) - canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N)) + noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => RubySaml::XML::Crypto::DSIG) + canon_string = noko_signed_info_element.canonicalize(RubySaml::XML::Crypto.canon_algorithm(RubySaml::XML::Crypto::C14N)) - signature = compute_signature(private_key, algorithm(signature_method).new, canon_string) + signature = compute_signature(private_key, RubySaml::XML::Crypto.hash_algorithm(signature_method).new, canon_string) signature_element.add_element('ds:SignatureValue').text = signature # add KeyInfo - key_info_element = signature_element.add_element('ds:KeyInfo') - x509_element = key_info_element.add_element('ds:X509Data') - x509_cert_element = x509_element.add_element('ds:X509Certificate') + key_info_element = signature_element.add_element('ds:KeyInfo') + x509_element = key_info_element.add_element('ds:X509Data') + x509_cert_element = x509_element.add_element('ds:X509Certificate') if certificate.is_a?(String) certificate = OpenSSL::X509::Certificate.new(certificate) end @@ -92,10 +103,10 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA256, diges end end - protected + private - def compute_signature(private_key, signature_algorithm, document) - Base64.encode64(private_key.sign(signature_algorithm, document)).gsub(/\n/, '') + def compute_signature(private_key, signature_hash_algorithm, document) + Base64.encode64(private_key.sign(signature_hash_algorithm, document)).gsub(/\n/, '') end def compute_digest(document, digest_algorithm) diff --git a/lib/ruby_saml/xml/signed_document.rb b/lib/ruby_saml/xml/signed_document.rb index 444a062a..15583f07 100644 --- a/lib/ruby_saml/xml/signed_document.rb +++ b/lib/ruby_saml/xml/signed_document.rb @@ -24,8 +24,8 @@ def validate_document(idp_cert_fingerprint, soft = true, options = {}) # get cert from response cert_element = REXML::XPath.first( self, - "//ds:X509Certificate", - { "ds"=>DSIG } + '//ds:X509Certificate', + { 'ds' => RubySaml::XML::Crypto::DSIG } ) if cert_element @@ -38,7 +38,7 @@ def validate_document(idp_cert_fingerprint, soft = true, options = {}) end if options[:fingerprint_alg] - fingerprint_alg = RubySaml::XML::BaseDocument.new.algorithm(options[:fingerprint_alg]).new + fingerprint_alg = RubySaml::XML::Crypto.hash_algorithm(options[:fingerprint_alg]).new else fingerprint_alg = OpenSSL::Digest.new('SHA256') end @@ -63,7 +63,7 @@ def validate_document_with_cert(idp_cert, soft = true) cert_element = REXML::XPath.first( self, '//ds:X509Certificate', - { 'ds'=>DSIG } + { 'ds' => RubySaml::XML::Crypto::DSIG } ) if cert_element @@ -97,34 +97,34 @@ def validate_signature(base64_cert, soft = true) sig_element = REXML::XPath.first( @working_copy, '//ds:Signature', - {'ds'=>DSIG} + { 'ds' => RubySaml::XML::Crypto::DSIG } ) # signature method sig_alg_value = REXML::XPath.first( sig_element, './ds:SignedInfo/ds:SignatureMethod', - {'ds'=>DSIG} + { 'ds' => RubySaml::XML::Crypto::DSIG } ) - signature_algorithm = algorithm(sig_alg_value) + signature_hash_algorithm = RubySaml::XML::Crypto.hash_algorithm(sig_alg_value) # get signature base64_signature = REXML::XPath.first( sig_element, './ds:SignatureValue', - {'ds' => DSIG} + { 'ds' => RubySaml::XML::Crypto::DSIG} ) signature = Base64.decode64(RubySaml::Utils.element_text(base64_signature)) # canonicalization method - canon_algorithm = canon_algorithm REXML::XPath.first( + canon_algorithm = RubySaml::XML::Crypto.canon_algorithm(REXML::XPath.first( sig_element, './ds:SignedInfo/ds:CanonicalizationMethod', - 'ds' => DSIG - ) + 'ds' => RubySaml::XML::Crypto::DSIG + )) - noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG) - noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG) + noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => RubySaml::XML::Crypto::DSIG) + noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => RubySaml::XML::Crypto::DSIG) canon_string = noko_signed_info_element.canonicalize(canon_algorithm) noko_sig_element.remove @@ -133,30 +133,30 @@ def validate_signature(base64_cert, soft = true) inclusive_namespaces = extract_inclusive_namespaces # check digests - ref = REXML::XPath.first(sig_element, '//ds:Reference', {'ds'=>DSIG}) + ref = REXML::XPath.first(sig_element, '//ds:Reference', { 'ds' => RubySaml::XML::Crypto::DSIG }) hashed_element = document.at_xpath('//*[@ID=$id]', nil, { 'id' => extract_signed_element_id }) - canon_algorithm = canon_algorithm REXML::XPath.first( + canon_algorithm = RubySaml::XML::Crypto.canon_algorithm(REXML::XPath.first( ref, '//ds:CanonicalizationMethod', - { 'ds' => DSIG } - ) + { 'ds' => RubySaml::XML::Crypto::DSIG } + )) canon_algorithm = process_transforms(ref, canon_algorithm) canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces) - digest_algorithm = algorithm(REXML::XPath.first( + digest_algorithm = RubySaml::XML::Crypto.hash_algorithm(REXML::XPath.first( ref, '//ds:DigestMethod', - { 'ds' => DSIG } + { 'ds' => RubySaml::XML::Crypto::DSIG } )) hash = digest_algorithm.digest(canon_hashed_element) encoded_digest_value = REXML::XPath.first( ref, '//ds:DigestValue', - { 'ds' => DSIG } + { 'ds' => RubySaml::XML::Crypto::DSIG } ) digest_value = Base64.decode64(RubySaml::Utils.element_text(encoded_digest_value)) @@ -169,7 +169,7 @@ def validate_signature(base64_cert, soft = true) cert = OpenSSL::X509::Certificate.new(cert_text) # verify signature - unless cert.public_key.verify(signature_algorithm.new, signature, canon_string) + unless cert.public_key.verify(signature_hash_algorithm.new, signature, canon_string) return append_error('Key validation error', soft) end @@ -182,24 +182,13 @@ def process_transforms(ref, canon_algorithm) transforms = REXML::XPath.match( ref, '//ds:Transforms/ds:Transform', - { 'ds' => DSIG } + { 'ds' => RubySaml::XML::Crypto::DSIG } ) transforms.each do |transform_element| - next unless transform_element.attributes && transform_element.attributes['Algorithm'] - - algorithm = transform_element.attributes['Algorithm'] - case algorithm - when 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315', - 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments' - canon_algorithm = Nokogiri::XML::XML_C14N_1_0 - when 'http://www.w3.org/2006/12/xml-c14n11', - 'http://www.w3.org/2006/12/xml-c14n11#WithComments' - canon_algorithm = Nokogiri::XML::XML_C14N_1_1 - when 'http://www.w3.org/2001/10/xml-exc-c14n#', - 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments' - canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0 - end + next unless transform_element.attributes&.[]('Algorithm') + + canon_algorithm = RubySaml::XML::Crypto.canon_algorithm(transform_element, default: false) end canon_algorithm @@ -213,7 +202,7 @@ def extract_signed_element_id reference_element = REXML::XPath.first( self, '//ds:Signature/ds:SignedInfo/ds:Reference', - {'ds'=>DSIG} + { 'ds' => RubySaml::XML::Crypto::DSIG } ) return nil if reference_element.nil? @@ -226,7 +215,7 @@ def extract_inclusive_namespaces element = REXML::XPath.first( self, '//ec:InclusiveNamespaces', - { 'ec' => C14N } + { 'ec' => RubySaml::XML::Crypto::C14N } ) return unless element diff --git a/test/helpers/certificate_helper.rb b/test/helpers/certificate_helper.rb index e826fbfb..64cf2466 100644 --- a/test/helpers/certificate_helper.rb +++ b/test/helpers/certificate_helper.rb @@ -3,28 +3,30 @@ 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) + def generate_pair(algorithm = :rsa, digest: nil, not_before: nil, not_after: nil) + key = generate_private_key(algorithm) + cert = generate_cert(key, digest: digest, 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 } + def generate_pem_array(algorithm = :rsa, not_before: nil, not_after: nil) + cert, key = generate_pair(algorithm, not_before: not_before, not_after: not_after) + [cert.to_pem, key.to_pem] end - def generate_key - OpenSSL::PKey::RSA.new(1024) + def generate_pem_hash(algorithm = :rsa, not_before: nil, not_after: nil) + cert, key = generate_pair(algorithm, not_before: not_before, not_after: not_after) + { certificate: cert.to_pem, private_key: key.to_pem } end - def generate_cert(key = generate_key, not_before: nil, not_after: nil) + def generate_cert(algorithm = :rsa, digest: nil, not_before: nil, not_after: nil) + key = generate_private_key(algorithm) 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.public_key = generate_public_key(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 @@ -32,10 +34,35 @@ def generate_cert(key = generate_key, not_before: nil, not_after: nil) 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.sign(key, generate_digest(digest)) cert end + def generate_private_key(algorithm = :rsa) + case algorithm + when OpenSSL::PKey::PKey + algorithm + when :dsa + OpenSSL::PKey::DSA.new(2048) + when :ec, :ecdsa + OpenSSL::PKey::EC.generate('prime256v1') + else + OpenSSL::PKey::RSA.new(2048) + end + end + + def generate_public_key(private_key) + private_key.is_a?(OpenSSL::PKey::EC) ? private_key : private_key.public_key + end + + def generate_digest(digest) + case digest + when OpenSSL::Digest then digest + when NilClass then OpenSSL::Digest.new('SHA256') + else OpenSSL::Digest.new(digest.to_s.upcase) + end + end + private def one_year diff --git a/test/idp_metadata_parser_test.rb b/test/idp_metadata_parser_test.rb index 1e9067ff..caad52a9 100644 --- a/test/idp_metadata_parser_test.rb +++ b/test/idp_metadata_parser_test.rb @@ -163,8 +163,8 @@ def initialize; end } }) assert_equal "C4:C6:BD:41:EC:AD:57:97:CE:7B:7D:80:06:C3:E4:30:53:29:02:0B:DD:2D:47:02:9E:BD:85:AD:93:02:45:21", settings.idp_cert_fingerprint - assert_equal RubySaml::XML::Document::SHA256, settings.security[:digest_method] - assert_equal RubySaml::XML::Document::RSA_SHA256, settings.security[:signature_method] + assert_equal RubySaml::XML::Document::SHA256, settings.get_sp_digest_method + assert_equal RubySaml::XML::Document::RSA_SHA256, settings.get_sp_signature_method end it "merges results into given settings object" do @@ -176,8 +176,8 @@ def initialize; end RubySaml::IdpMetadataParser.new.parse(idp_metadata_descriptor, :settings => settings) assert_equal "C4:C6:BD:41:EC:AD:57:97:CE:7B:7D:80:06:C3:E4:30:53:29:02:0B:DD:2D:47:02:9E:BD:85:AD:93:02:45:21", settings.idp_cert_fingerprint - assert_equal RubySaml::XML::Document::SHA256, settings.security[:digest_method] - assert_equal RubySaml::XML::Document::RSA_SHA256, settings.security[:signature_method] + assert_equal RubySaml::XML::Document::SHA256, settings.get_sp_digest_method + assert_equal RubySaml::XML::Document::RSA_SHA256, settings.get_sp_signature_method end end diff --git a/test/logoutrequest_test.rb b/test/logoutrequest_test.rb index a1368249..a35b8406 100644 --- a/test/logoutrequest_test.rb +++ b/test/logoutrequest_test.rb @@ -109,248 +109,257 @@ class RequestTest < Minitest::Test end end - describe "signing with HTTP-POST binding" do - before do - settings.security[:logout_requests_signed] = true - settings.idp_slo_service_binding = :post - settings.idp_sso_service_binding = :redirect - settings.certificate = ruby_saml_cert_text - settings.private_key = ruby_saml_key_text + describe "#manipulate request_id" do + it "be able to modify the request id" do + logoutrequest = RubySaml::Logoutrequest.new + request_id = logoutrequest.request_id + assert_equal request_id, logoutrequest.uuid + logoutrequest.uuid = "new_uuid" + assert_equal logoutrequest.request_id, logoutrequest.uuid + assert_equal "new_uuid", logoutrequest.request_id end + end - it "doesn't sign through create_xml_document" do - unauth_req = RubySaml::Logoutrequest.new - inflated = unauth_req.create_xml_document(settings).to_s - - refute_match %r[([a-zA-Z0-9/+=]+)], inflated - refute_match %r[], inflated - refute_match %r[], inflated - end + with_each_key_algorithm do |algorithm| + describe "signing with HTTP-POST binding" do + before do + settings.security[:logout_requests_signed] = true + settings.idp_slo_service_binding = :post + settings.idp_sso_service_binding = :redirect + settings.certificate, settings.private_key = CertificateHelper.generate_pem_array(algorithm) + end - it "sign unsigned request" do - unauth_req = RubySaml::Logoutrequest.new - unauth_req_doc = unauth_req.create_xml_document(settings) - inflated = unauth_req_doc.to_s + it "doesn't sign through create_xml_document" do + unauth_req = RubySaml::Logoutrequest.new + inflated = unauth_req.create_xml_document(settings).to_s - refute_match %r[([a-zA-Z0-9/+=]+)], inflated - refute_match %r[], inflated - refute_match %r[], inflated + refute_match %r[([a-zA-Z0-9/+=]+)], inflated + refute_match signature_method_matcher(algorithm), inflated + refute_match %r[], inflated + end - inflated = unauth_req.sign_document(unauth_req_doc, settings).to_s + it "sign unsigned request" do + unauth_req = RubySaml::Logoutrequest.new + unauth_req_doc = unauth_req.create_xml_document(settings) + inflated = unauth_req_doc.to_s - assert_match %r[([a-zA-Z0-9/+=]+)], inflated - assert_match %r[], inflated - assert_match %r[], inflated - end + refute_match %r[([a-zA-Z0-9/+=]+)], inflated + refute_match signature_method_matcher(algorithm), inflated + refute_match %r[], inflated - it "signs through create_logout_request_xml_doc" do - unauth_req = RubySaml::Logoutrequest.new - inflated = unauth_req.create_logout_request_xml_doc(settings).to_s + inflated = unauth_req.sign_document(unauth_req_doc, settings).to_s - assert_match %r[([a-zA-Z0-9/+=]+)], inflated - assert_match %r[], inflated - assert_match %r[], inflated - end + assert_match %r[([a-zA-Z0-9/+=]+)], inflated + assert_match signature_method_matcher(algorithm), inflated + assert_match %r[], inflated + end - it "create an uncompressed signed logout request" do - params = RubySaml::Logoutrequest.new.create_params(settings) - request_xml = Base64.decode64(params["SAMLRequest"]) + it "signs through create_logout_request_xml_doc" do + unauth_req = RubySaml::Logoutrequest.new + inflated = unauth_req.create_logout_request_xml_doc(settings).to_s - assert_match %r[([a-zA-Z0-9/+=]+)], request_xml - assert_match %r[], request_xml - assert_match %r[], request_xml - end + assert_match %r[([a-zA-Z0-9/+=]+)], inflated + assert_match signature_method_matcher(algorithm), inflated + assert_match %r[], inflated + end - it "create a signed logout request with 256 digest and signature method" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 - settings.security[:digest_method] = RubySaml::XML::Document::SHA256 + it "create an uncompressed signed logout request" do + params = RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) - params = 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 + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match signature_method_matcher(algorithm), request_xml + assert_match %r[], request_xml + end - it "create a signed logout request with 512 digest and signature method RSA_SHA384" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA384 - settings.security[:digest_method] = RubySaml::XML::Document::SHA512 + it "create a signed logout request with SHA256 digest and signature method RSA_SHA256" do + # RSA is ignored here; only the hash algorithm is used + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 + settings.security[:digest_method] = RubySaml::XML::Document::SHA256 - params = RubySaml::Logoutrequest.new.create_params(settings) - request_xml = Base64.decode64(params["SAMLRequest"]) + params = RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match signature_method_matcher(algorithm), request_xml + assert_match %r[], request_xml + end - 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 SHA512 digest and signature method RSA_SHA384" do + skip('DSA does not support SHA384/SHA512') if algorithm == :dsa - it "create a signed logout 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 = 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 + # RSA is ignored here; only the hash algorithm is used + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA384 + settings.security[:digest_method] = RubySaml::XML::Document::SHA512 - it "create a signed logout 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 = 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 + params = RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) - it "raises error when no valid certs and :check_sp_cert_expiration is true" do - settings.security[:check_sp_cert_expiration] = true + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match signature_method_matcher(algorithm, :sha384), request_xml + assert_match %r[], request_xml + end - assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do - RubySaml::Logoutrequest.new.create_params(settings) + it "create a signed logout request using the first certificate and key" do + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + CertificateHelper.generate_pem_hash(algorithm), + CertificateHelper.generate_pem_hash + ] + } + + params = RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match signature_method_matcher(algorithm), request_xml + assert_match %r[], request_xml end - end - end - describe "signing with HTTP-Redirect binding" do + it "create a signed logout request using the first valid certificate and key when :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + CertificateHelper.generate_pem_hash(algorithm), + CertificateHelper.generate_pem_hash + ] + } + + params = RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match signature_method_matcher(algorithm), request_xml + assert_match %r[], request_xml + end - let(:cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) } + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.certificate, settings.private_key = CertificateHelper.generate_pem_array(algorithm, not_after: Time.now - 60) + settings.security[:check_sp_cert_expiration] = true - before do - settings.security[:logout_requests_signed] = true - settings.idp_slo_service_binding = :redirect - settings.idp_sso_service_binding = :post - settings.certificate = ruby_saml_cert_text - settings.private_key = ruby_saml_key_text + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::Logoutrequest.new.create_params(settings) + end + end end - it "create a signature parameter with RSA_SHA1 / SHA1 and validate it" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 + describe "signing with HTTP-Redirect binding" do - params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') - assert params['SAMLRequest'] - assert params[:RelayState] - assert params['Signature'] - assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA1 + let(:cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) } - query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" - query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" - query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + before do + settings.security[:logout_requests_signed] = true + settings.idp_slo_service_binding = :redirect + settings.idp_sso_service_binding = :post + settings.certificate = ruby_saml_cert_text + settings.private_key = ruby_saml_key_text + end - signature_algorithm = RubySaml::XML::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 "create a signature parameter with RSA_SHA1 / SHA1 and validate it" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 - it "create a signature parameter with RSA_SHA256 / SHA256 and validate it" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['SAMLRequest'] + assert params[:RelayState] + assert params['Signature'] + assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA1 - params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') - assert params['Signature'] - assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA256 + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" - query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" - query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" - query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + signature_algorithm = RubySaml::XML::Crypto.hash_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 - signature_algorithm = RubySaml::XML::BaseDocument.new.algorithm(params['SigAlg']) - 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 with RSA_SHA256 / SHA256 and validate it" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 - it "create a signature parameter with RSA_SHA384 / SHA384 and validate it" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA384 + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['Signature'] + assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA256 - params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') - assert params['Signature'] - assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA384 + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" - query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" - query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" - query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + signature_algorithm = RubySaml::XML::Crypto.hash_algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA256 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end - signature_algorithm = RubySaml::XML::BaseDocument.new.algorithm(params['SigAlg']) - assert_equal signature_algorithm, OpenSSL::Digest::SHA384 - assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) - end + it "create a signature parameter with RSA_SHA384 / SHA384 and validate it" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA384 - it "create a signature parameter with RSA_SHA512 / SHA512 and validate it" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA512 + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['Signature'] + assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA384 - params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') - assert params['Signature'] - assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA512 + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" - query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" - query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" - query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + signature_algorithm = RubySaml::XML::Crypto.hash_algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA384 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end - signature_algorithm = RubySaml::XML::BaseDocument.new.algorithm(params['SigAlg']) - 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 with RSA_SHA512 / SHA512 and validate it" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA512 - it "create a signature parameter using the first certificate and key" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 - 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 = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') - assert params['SAMLRequest'] - assert params[:RelayState] - assert params['Signature'] - assert_equal params['SigAlg'], RubySaml::XML::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 = RubySaml::XML::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 + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['Signature'] + assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA512 - it "raises error when no valid certs and :check_sp_cert_expiration is true" do - settings.security[:check_sp_cert_expiration] = true + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" - assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do - RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + signature_algorithm = RubySaml::XML::Crypto.hash_algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA512 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) end - end - end - describe "#manipulate request_id" do - it "be able to modify the request id" do - logoutrequest = RubySaml::Logoutrequest.new - request_id = logoutrequest.request_id - assert_equal request_id, logoutrequest.uuid - logoutrequest.uuid = "new_uuid" - assert_equal logoutrequest.request_id, logoutrequest.uuid - assert_equal "new_uuid", logoutrequest.request_id + it "create a signature parameter using the first certificate and key" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 + settings.certificate = nil + settings.private_key = nil + cert, pkey = CertificateHelper.generate_pair(algorithm) + settings.sp_cert_multi = { + signing: [ + { certificate: cert.to_pem, private_key: pkey.to_pem }, + CertificateHelper.generate_pem_hash + ] + } + + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['SAMLRequest'] + assert params[:RelayState] + assert params['Signature'] + assert_equal params['SigAlg'], signature_method(algorithm, :sha1) + + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + + signature_algorithm = RubySaml::XML::Crypto.hash_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.certificate, settings.private_key = CertificateHelper.generate_pem_array(algorithm, not_after: Time.now - 60) + settings.security[:check_sp_cert_expiration] = true + + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + end + end end end end diff --git a/test/logoutresponse_test.rb b/test/logoutresponse_test.rb index 55f41901..cde820c8 100644 --- a/test/logoutresponse_test.rb +++ b/test/logoutresponse_test.rb @@ -314,7 +314,7 @@ class RubySamlTest < Minitest::Test refute_equal(query, original_query) assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. - sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm(settings.security[:signature_method]) + sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method) 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, @@ -350,7 +350,7 @@ class RubySamlTest < Minitest::Test refute_equal(query, original_query) assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. - sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm(settings.security[:signature_method]) + sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method) 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, diff --git a/test/metadata_test.rb b/test/metadata_test.rb index b97b8b38..d8831308 100644 --- a/test/metadata_test.rb +++ b/test/metadata_test.rb @@ -232,9 +232,9 @@ class MetadataTest < Minitest::Test 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) + valid_pair = CertificateHelper.generate_pem_hash + early_pair = CertificateHelper.generate_pem_hash(not_before: Time.now + 60) + expired_pair = CertificateHelper.generate_pem_hash(not_after: Time.now - 60) settings.certificate = nil settings.certificate_new = nil settings.private_key = nil @@ -328,82 +328,85 @@ class MetadataTest < Minitest::Test end end - describe "when the settings indicate to sign (embedded) metadata" do - before do - settings.security[:metadata_signed] = true - settings.certificate = ruby_saml_cert_text - settings.private_key = ruby_saml_key_text - end - - it "creates a signed metadata" do - assert_match %r[([a-zA-Z0-9/+=]+)]m, xml_text - assert_match %r[], xml_text - assert_match %r[], xml_text - - signed_metadata = RubySaml::XML::SignedDocument.new(xml_text) - assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false) - - assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") - end - - describe "when digest and signature methods are specified" do + with_each_key_algorithm do |algorithm| + describe "when the settings indicate to sign (embedded) metadata" do before do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 - settings.security[:digest_method] = RubySaml::XML::Document::SHA512 + settings.security[:metadata_signed] = true + cert, pkey = CertificateHelper.generate_pair(algorithm) + @fingerprint = OpenSSL::Digest.new('SHA256', cert.to_der).to_s + settings.certificate, settings.private_key = [cert, pkey].map(&:to_pem) end - it "creates a signed metadata with specified digest and signature methods" do + it "creates a signed metadata" do assert_match %r[([a-zA-Z0-9/+=]+)]m, xml_text - assert_match %r[], xml_text - assert_match %r[], xml_text + assert_match signature_method_matcher(algorithm), xml_text + assert_match %r[], xml_text signed_metadata = RubySaml::XML::SignedDocument.new(xml_text) - assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false) + assert signed_metadata.validate_document(@fingerprint, false) assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") end - end - describe "when custom metadata elements have been inserted" do - let(:xml_text) { subclass.new.generate(settings, false) } - let(:subclass) do - Class.new(RubySaml::Metadata) do - def add_extras(root, _settings) - idp = REXML::Element.new("md:IDPSSODescriptor") - idp.attributes['protocolSupportEnumeration'] = 'urn:oasis:names:tc:SAML:2.0:protocol' - - nid = REXML::Element.new("md:NameIDFormat") - nid.text = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' - idp.add_element(nid) - - sso = REXML::Element.new("md:SingleSignOnService") - sso.attributes['Binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' - sso.attributes['Location'] = 'https://foobar.com/sso' - idp.add_element(sso) - root.insert_before(root.children[0], idp) - - org = REXML::Element.new("md:Organization") - org.add_element("md:OrganizationName", 'xml:lang' => "en-US").text = 'ACME Inc.' - org.add_element("md:OrganizationDisplayName", 'xml:lang' => "en-US").text = 'ACME' - org.add_element("md:OrganizationURL", 'xml:lang' => "en-US").text = 'https://www.acme.com' - root.insert_after(root.children[3], org) - end + describe "when digest and signature methods are specified" do + before do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 + settings.security[:digest_method] = RubySaml::XML::Document::SHA512 + end + + it "creates a signed metadata with specified digest and signature methods" do + assert_match %r[([a-zA-Z0-9/+=]+)]m, xml_text + assert_match signature_method_matcher(algorithm), xml_text + assert_match %r[], xml_text + + signed_metadata = RubySaml::XML::SignedDocument.new(xml_text) + assert signed_metadata.validate_document(@fingerprint, false) + + assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") end end - it "inserts signature as the first child of root element" do - first_child = xml_doc.root.children[0] - assert_equal first_child.prefix, 'ds' - assert_equal first_child.name, 'Signature' + describe "when custom metadata elements have been inserted" do + let(:xml_text) { subclass.new.generate(settings, false) } + let(:subclass) do + Class.new(RubySaml::Metadata) do + def add_extras(root, _settings) + idp = REXML::Element.new("md:IDPSSODescriptor") + idp.attributes['protocolSupportEnumeration'] = 'urn:oasis:names:tc:SAML:2.0:protocol' + + nid = REXML::Element.new("md:NameIDFormat") + nid.text = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + idp.add_element(nid) + + sso = REXML::Element.new("md:SingleSignOnService") + sso.attributes['Binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + sso.attributes['Location'] = 'https://foobar.com/sso' + idp.add_element(sso) + root.insert_before(root.children[0], idp) + + org = REXML::Element.new("md:Organization") + org.add_element("md:OrganizationName", 'xml:lang' => "en-US").text = 'ACME Inc.' + org.add_element("md:OrganizationDisplayName", 'xml:lang' => "en-US").text = 'ACME' + org.add_element("md:OrganizationURL", 'xml:lang' => "en-US").text = 'https://www.acme.com' + root.insert_after(root.children[3], org) + end + end + end - assert_match %r[([a-zA-Z0-9/+=]+)]m, xml_text - assert_match %r[], xml_text - assert_match %r[], xml_text + it "inserts signature as the first child of root element" do + first_child = xml_doc.root.children[0] + assert_equal first_child.prefix, 'ds' + assert_equal first_child.name, 'Signature' - signed_metadata = RubySaml::XML::SignedDocument.new(xml_text) - assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false) + assert_match %r[([a-zA-Z0-9/+=]+)]m, xml_text + assert_match signature_method_matcher(algorithm), xml_text + assert_match %r[], xml_text - assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") + signed_metadata = RubySaml::XML::SignedDocument.new(xml_text) + assert signed_metadata.validate_document(@fingerprint, false) + + assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") + end end end end diff --git a/test/request_test.rb b/test/request_test.rb index bf47b54e..987b0e7f 100644 --- a/test/request_test.rb +++ b/test/request_test.rb @@ -27,8 +27,6 @@ class RequestTest < Minitest::Test end it "create the deflated SAMLRequest URL parameter including the Destination" do - skip "This test fails on this specific JRuby version" if defined?(JRUBY_VERSION) && JRUBY_VERSION == "9.2.17.0" - auth_url = RubySaml::Authrequest.new.create(settings) payload = CGI.unescape(auth_url.split("=").last) decoded = Base64.decode64(payload) @@ -240,159 +238,6 @@ class RequestTest < Minitest::Test assert_match(/urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport<\/saml:AuthnContextDeclRef>/, auth_doc.to_s) end - describe "#create_params signing with HTTP-POST binding" do - before do - settings.idp_sso_service_url = "http://example.com?field=value" - settings.idp_sso_service_binding = :post - settings.security[:authn_requests_signed] = true - settings.certificate = ruby_saml_cert_text - settings.private_key = ruby_saml_key_text - end - - it "create a signed request" do - params = 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 "create a signed request with 256 digest and signature methods" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 - settings.security[:digest_method] = RubySaml::XML::Document::SHA512 - - params = 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 - 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 = 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 = 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(RubySaml::ValidationError, 'The SP certificate expired.') do - RubySaml::Authrequest.new.create_params(settings) - end - end - end - - describe "#create_params signing with HTTP-Redirect binding" do - let(:cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) } - - before do - settings.idp_sso_service_url = "http://example.com?field=value" - settings.idp_sso_service_binding = :redirect - settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" - settings.security[:authn_requests_signed] = true - settings.certificate = ruby_saml_cert_text - settings.private_key = ruby_saml_key_text - end - - it "create a signature parameter with RSA_SHA1 and validate it" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 - - params = RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') - assert params['SAMLRequest'] - assert params[:RelayState] - assert params['Signature'] - assert_equal params['SigAlg'], RubySaml::XML::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 = RubySaml::XML::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 "create a signature parameter with RSA_SHA256 and validate it" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 - - params = RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') - assert params['Signature'] - assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA256 - - query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" - query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" - query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" - - signature_algorithm = RubySaml::XML::BaseDocument.new.algorithm(params['SigAlg']) - 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] = RubySaml::XML::Document::RSA_SHA1 - 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 = RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') - assert params['SAMLRequest'] - assert params[:RelayState] - assert params['Signature'] - assert_equal params['SigAlg'], RubySaml::XML::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 = RubySaml::XML::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(RubySaml::ValidationError, 'The SP certificate expired.') do - RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') - end - end - end - it "create the saml:AuthnContextClassRef element correctly" do settings.authn_context = 'secure/name/password/uri' auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) @@ -437,5 +282,165 @@ class RequestTest < Minitest::Test assert_equal "new_uuid", authnrequest.request_id end end + + with_each_key_algorithm do |algorithm| + describe "#create_params signing with HTTP-POST binding" do + before do + settings.idp_sso_service_url = "http://example.com?field=value" + settings.idp_sso_service_binding = :post + settings.security[:authn_requests_signed] = true + settings.certificate, settings.private_key = CertificateHelper.generate_pem_array(algorithm) + end + + it "create a signed request" do + params = RubySaml::Authrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match signature_method_matcher(algorithm), request_xml + assert_match %r[], request_xml + end + + it "create a signed request with 256 digest and signature methods" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 + settings.security[:digest_method] = RubySaml::XML::Document::SHA512 + + params = RubySaml::Authrequest.new.create_params(settings) + + request_xml = Base64.decode64(params["SAMLRequest"]) + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match signature_method_matcher(algorithm), 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: [ + CertificateHelper.generate_pem_hash(algorithm), + CertificateHelper.generate_pem_hash + ] + } + + params = RubySaml::Authrequest.new.create_params(settings) + + request_xml = Base64.decode64(params["SAMLRequest"]) + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match signature_method_matcher(algorithm), 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: [ + CertificateHelper.generate_pem_hash(algorithm), + CertificateHelper.generate_pem_hash + ] + } + + params = RubySaml::Authrequest.new.create_params(settings) + + request_xml = Base64.decode64(params["SAMLRequest"]) + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match signature_method_matcher(algorithm), request_xml + assert_match %r[], request_xml + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.certificate, settings.private_key = CertificateHelper.generate_pem_array(algorithm, not_after: Time.now - 60) + settings.security[:check_sp_cert_expiration] = true + + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::Authrequest.new.create_params(settings) + end + end + end + + describe "#create_params signing with HTTP-Redirect binding" do + let(:cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) } + + before do + settings.idp_sso_service_url = "http://example.com?field=value" + settings.idp_sso_service_binding = :redirect + settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" + settings.security[:authn_requests_signed] = true + settings.certificate = ruby_saml_cert_text + settings.private_key = ruby_saml_key_text + end + + it "create a signature parameter with RSA_SHA1 and validate it" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 + + params = RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['SAMLRequest'] + assert params[:RelayState] + assert params['Signature'] + assert_equal params['SigAlg'], RubySaml::XML::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 = RubySaml::XML::Crypto.hash_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 "create a signature parameter with RSA_SHA256 and validate it" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 + + params = RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['Signature'] + assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA256 + + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + + signature_algorithm = RubySaml::XML::Crypto.hash_algorithm(params['SigAlg']) + 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] = RubySaml::XML::Document::RSA_SHA1 + settings.certificate = nil + settings.private_key = nil + cert, pkey = CertificateHelper.generate_pair(algorithm) + settings.sp_cert_multi = { + signing: [ + { certificate: cert.to_pem, private_key: pkey.to_pem }, + CertificateHelper.generate_pem_hash + ] + } + + params = RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['SAMLRequest'] + assert params[:RelayState] + assert params['Signature'] + assert_equal params['SigAlg'], signature_method(algorithm, :sha1) + + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + + signature_algorithm = RubySaml::XML::Crypto.hash_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.certificate, settings.private_key = CertificateHelper.generate_pem_array(algorithm, not_after: Time.now - 60) + settings.security[:check_sp_cert_expiration] = true + + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + end + end + end + end end end diff --git a/test/response_test.rb b/test/response_test.rb index 2eef8464..928e30ee 100644 --- a/test/response_test.rb +++ b/test/response_test.rb @@ -1602,7 +1602,7 @@ def generate_audience_error(expected, actual) settings.private_key = nil settings.sp_cert_multi = { encryption: [ - CertificateHelper.generate_pair_hash, + CertificateHelper.generate_pem_hash, { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text } ] } diff --git a/test/settings_test.rb b/test/settings_test.rb index 21f4b6f8..9bb13d2c 100644 --- a/test/settings_test.rb +++ b/test/settings_test.rb @@ -100,8 +100,8 @@ class SettingsTest < Minitest::Test new_settings = RubySaml::Settings.new assert_equal new_settings.security[:authn_requests_signed], false - assert_equal new_settings.security[:digest_method], RubySaml::XML::Document::SHA256 - assert_equal new_settings.security[:signature_method], RubySaml::XML::Document::RSA_SHA256 + assert_equal new_settings.get_sp_digest_method, RubySaml::XML::Document::SHA256 + assert_equal new_settings.get_sp_signature_method, RubySaml::XML::Document::RSA_SHA256 end it "overrides only provided security attributes passing a second parameter" do @@ -420,7 +420,7 @@ class SettingsTest < Minitest::Test 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 } + let(:key_text2) { CertificateHelper.generate_private_key.to_pem } it "returns certs for single case" do @settings.certificate = cert_text1 @@ -570,9 +570,9 @@ class SettingsTest < Minitest::Test 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) } + let(:valid_pair) { CertificateHelper.generate_pem_hash } + let(:early_pair) { CertificateHelper.generate_pem_hash(not_before: Time.now + 60) } + let(:expired_pair) { CertificateHelper.generate_pem_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 } @@ -623,9 +623,9 @@ class SettingsTest < Minitest::Test 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) } + let(:valid_pair) { CertificateHelper.generate_pem_hash } + let(:early_pair) { CertificateHelper.generate_pem_hash(not_before: Time.now + 60) } + let(:expired) { CertificateHelper.generate_pem_hash(not_after: Time.now - 60) } it "returns nil when no signing pairs are present" do @settings.sp_cert_multi = { signing: [] } @@ -665,9 +665,9 @@ class SettingsTest < Minitest::Test 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) } + let(:valid_pair) { CertificateHelper.generate_pem_hash } + let(:early_pair) { CertificateHelper.generate_pem_hash(not_before: Time.now + 60) } + let(:expired_pair) { CertificateHelper.generate_pem_hash(not_after: Time.now - 60) } it "returns an empty array when no decryption pairs are present" do @settings.sp_cert_multi = { encryption: [] } diff --git a/test/slo_logoutrequest_test.rb b/test/slo_logoutrequest_test.rb index f5777270..56d774ed 100644 --- a/test/slo_logoutrequest_test.rb +++ b/test/slo_logoutrequest_test.rb @@ -401,7 +401,7 @@ class RubySamlTest < Minitest::Test refute_equal(query, original_query) assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. - sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm(settings.security[:signature_method]) + sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method) 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. @@ -436,7 +436,7 @@ class RubySamlTest < Minitest::Test refute_equal(query, original_query) assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. - sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm(settings.security[:signature_method]) + sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method) 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. @@ -468,14 +468,12 @@ class RubySamlTest < Minitest::Test # send is based on this base64 request. params = { 'SAMLRequest' => downcased_escape(base64_request), - 'SigAlg' => downcased_escape(settings.security[:signature_method]), + 'SigAlg' => downcased_escape(settings.get_sp_signature_method), } # Assemble query string. query = "SAMLRequest=#{params['SAMLRequest']}&SigAlg=#{params['SigAlg']}" # Make normalised signature based on our modified params. - sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm( - settings.security[:signature_method] - ) + sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method) signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = downcased_escape(Base64.encode64(signature).gsub(/\n/, "")) diff --git a/test/slo_logoutresponse_test.rb b/test/slo_logoutresponse_test.rb index 421017f3..a840d9a8 100644 --- a/test/slo_logoutresponse_test.rb +++ b/test/slo_logoutresponse_test.rb @@ -97,260 +97,272 @@ class SloLogoutresponseTest < Minitest::Test end end - describe "signing with HTTP-POST binding" do - before do - settings.idp_sso_service_binding = :redirect - settings.idp_slo_service_binding = :post - settings.security[:logout_responses_signed] = true + describe "#manipulate response_id" do + it "be able to modify the response id" do + logoutresponse = RubySaml::SloLogoutresponse.new + response_id = logoutresponse.response_id + assert_equal response_id, logoutresponse.uuid + logoutresponse.uuid = "new_uuid" + assert_equal logoutresponse.response_id, logoutresponse.uuid + assert_equal "new_uuid", logoutresponse.response_id end + end - it "doesn't sign through create_xml_document" do - unauth_res = RubySaml::SloLogoutresponse.new - inflated = unauth_res.create_xml_document(settings).to_s + with_each_key_algorithm do |algorithm| + describe "signing with HTTP-POST binding" do + before do + settings.idp_sso_service_binding = :redirect + settings.idp_slo_service_binding = :post + settings.security[:logout_responses_signed] = true + settings.certificate, settings.private_key = CertificateHelper.generate_pem_array(algorithm) + end - refute_match %r[([a-zA-Z0-9/+=]+)], inflated - refute_match %r[], inflated - refute_match %r[], inflated - end + it "doesn't sign through create_xml_document" do + unauth_res = RubySaml::SloLogoutresponse.new + inflated = unauth_res.create_xml_document(settings).to_s - it "sign unsigned request" do - unauth_res = RubySaml::SloLogoutresponse.new - unauth_res_doc = unauth_res.create_xml_document(settings) - inflated = unauth_res_doc.to_s + refute_match %r[([a-zA-Z0-9/+=]+)], inflated + refute_match signature_method_matcher(algorithm), inflated + refute_match %r[], inflated + end - refute_match %r[([a-zA-Z0-9/+=]+)], inflated - refute_match %r[], inflated - refute_match %r[], inflated + it "sign unsigned request" do + unauth_res = RubySaml::SloLogoutresponse.new + unauth_res_doc = unauth_res.create_xml_document(settings) + inflated = unauth_res_doc.to_s - inflated = unauth_res.sign_document(unauth_res_doc, settings).to_s + refute_match %r[([a-zA-Z0-9/+=]+)], inflated + refute_match signature_method_matcher(algorithm), inflated + refute_match %r[], inflated - assert_match %r[([a-zA-Z0-9/+=]+)], inflated - assert_match %r[], inflated - assert_match %r[], inflated - end + inflated = unauth_res.sign_document(unauth_res_doc, settings).to_s - it "signs through create_logout_response_xml_doc" do - unauth_res = RubySaml::SloLogoutresponse.new - inflated = unauth_res.create_logout_response_xml_doc(settings).to_s + assert_match %r[([a-zA-Z0-9/+=]+)], inflated + assert_match signature_method_matcher(algorithm), inflated + assert_match %r[], inflated + end - assert_match %r[([a-zA-Z0-9/+=]+)], inflated - assert_match %r[], inflated - assert_match %r[], inflated - end + it "signs through create_logout_response_xml_doc" do + unauth_res = RubySaml::SloLogoutresponse.new + inflated = unauth_res.create_logout_response_xml_doc(settings).to_s - it "create a signed logout response" do - logout_request.settings = settings + assert_match %r[([a-zA-Z0-9/+=]+)], inflated + assert_match signature_method_matcher(algorithm), inflated + assert_match %r[], inflated + end - params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + it "create a signed logout response" do + logout_request.settings = settings - response_xml = Base64.decode64(params["SAMLResponse"]) - assert_match %r[([a-zA-Z0-9/+=]+)], response_xml - assert_match(//, response_xml) - assert_match(//, response_xml) - end + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") - it "create a signed logout response with SHA384 digest and signature method RSA_SHA512" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA512 - settings.security[:digest_method] = RubySaml::XML::Document::SHA384 - logout_request.settings = settings + response_xml = Base64.decode64(params["SAMLResponse"]) + assert_match %r[([a-zA-Z0-9/+=]+)], response_xml + assert_match(signature_method_matcher(algorithm), response_xml) + assert_match(//, response_xml) + end - params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + it "create a signed logout response with SHA384 digest and signature method RSA_SHA512" do + skip('DSA does not support SHA384/SHA512') if algorithm == :dsa - response_xml = Base64.decode64(params["SAMLResponse"]) - assert_match %r[([a-zA-Z0-9/+=]+)], response_xml - assert_match(//, response_xml) - assert_match(//, response_xml) - end + # RSA is ignored here; only the hash algorithm is used + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA512 + settings.security[:digest_method] = RubySaml::XML::Document::SHA384 + logout_request.settings = settings - it "create a signed logout response with SHA512 digest and signature method RSA_SHA384" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA384 - settings.security[:digest_method] = RubySaml::XML::Document::SHA512 - logout_request.settings = settings + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") - params = 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(signature_method_matcher(algorithm, :sha512), response_xml) + assert_match(//, response_xml) + end - 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 with SHA512 digest and signature method RSA_SHA384" do + skip('DSA does not support SHA384/SHA512') if algorithm == :dsa - 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 = 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 + # RSA is ignored here; only the hash algorithm is used + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA384 + settings.security[:digest_method] = RubySaml::XML::Document::SHA512 + logout_request.settings = settings - 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 = 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 + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") - 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 + response_xml = Base64.decode64(params["SAMLResponse"]) + assert_match %r[([a-zA-Z0-9/+=]+)], response_xml + assert_match(signature_method_matcher(algorithm, :sha384), response_xml) + assert_match(//, response_xml) + end - assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do - RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + 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: [ + CertificateHelper.generate_pem_hash(algorithm), + CertificateHelper.generate_pem_hash + ] + } + logout_request.settings = settings + + params = 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(signature_method_matcher(algorithm), response_xml) + assert_match(//, response_xml) end - end - end - describe "signing with HTTP-Redirect binding" do - let(:cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) } + 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: [ + CertificateHelper.generate_pem_hash(algorithm), + CertificateHelper.generate_pem_hash + ] + } + logout_request.settings = settings + + params = 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(signature_method_matcher(algorithm), response_xml) + assert_match(//, response_xml) + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.certificate, settings.private_key = CertificateHelper.generate_pem_array(algorithm, not_after: Time.now - 60) + settings.security[:check_sp_cert_expiration] = true + logout_request.settings = settings - before do - settings.idp_sso_service_binding = :post - settings.idp_slo_service_binding = :redirect - settings.security[:logout_responses_signed] = true + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + end + end end - it "create a signature parameter with RSA_SHA1 and validate it" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 + describe "signing with HTTP-Redirect binding" do + let(:cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) } - params = 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'], RubySaml::XML::Document::RSA_SHA1 + before do + settings.idp_sso_service_binding = :post + settings.idp_slo_service_binding = :redirect + settings.security[:logout_responses_signed] = true + end - query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" - query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" - query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + it "create a signature parameter with RSA_SHA1 and validate it" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 - signature_algorithm = RubySaml::XML::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 + params = 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'], RubySaml::XML::Document::RSA_SHA1 - it "create a signature parameter with RSA_SHA256 /SHA256 and validate it" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 + query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" - params = 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'] + signature_algorithm = RubySaml::XML::Crypto.hash_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 - assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA256 + it "create a signature parameter with RSA_SHA256 /SHA256 and validate it" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA256 - query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" - query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" - query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + params = 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'] - signature_algorithm = RubySaml::XML::BaseDocument.new.algorithm(params['SigAlg']) - assert_equal signature_algorithm, OpenSSL::Digest::SHA256 - assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) - end + assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA256 - it "create a signature parameter with RSA_SHA384 / SHA384 and validate it" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA384 + query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" - params = 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'] + signature_algorithm = RubySaml::XML::Crypto.hash_algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA256 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end - assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA384 + it "create a signature parameter with RSA_SHA384 / SHA384 and validate it" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA384 - query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" - query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" - query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + params = 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'] - signature_algorithm = RubySaml::XML::BaseDocument.new.algorithm(params['SigAlg']) - assert_equal signature_algorithm, OpenSSL::Digest::SHA384 - assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) - end + assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA384 - it "create a signature parameter with RSA_SHA512 / SHA512 and validate it" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA512 + query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" - params = 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'] + signature_algorithm = RubySaml::XML::Crypto.hash_algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA384 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end - assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA512 + it "create a signature parameter with RSA_SHA512 / SHA512 and validate it" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA512 - query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" - query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" - query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + params = 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'] - signature_algorithm = RubySaml::XML::BaseDocument.new.algorithm(params['SigAlg']) - assert_equal signature_algorithm, OpenSSL::Digest::SHA512 - assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) - end + assert_equal params['SigAlg'], RubySaml::XML::Document::RSA_SHA512 - it "create a signature parameter using the first certificate and key" do - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 - 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 = 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'], RubySaml::XML::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 = RubySaml::XML::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 + query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" - it "raises error when no valid certs and :check_sp_cert_expiration is true" do - settings.security[:check_sp_cert_expiration] = true + signature_algorithm = RubySaml::XML::Crypto.hash_algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA512 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end - assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do - RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + it "create a signature parameter using the first certificate and key" do + settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 + settings.certificate = nil + settings.private_key = nil + cert, pkey = CertificateHelper.generate_pair(algorithm) + settings.sp_cert_multi = { + signing: [ + { certificate: cert.to_pem, private_key: pkey.to_pem }, + CertificateHelper.generate_pem_hash + ] + } + + params = 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'], signature_method(algorithm, :sha1) + + query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + + signature_algorithm = RubySaml::XML::Crypto.hash_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 - end - end - describe "#manipulate response_id" do - it "be able to modify the response id" do - logoutresponse = RubySaml::SloLogoutresponse.new - response_id = logoutresponse.response_id - assert_equal response_id, logoutresponse.uuid - logoutresponse.uuid = "new_uuid" - assert_equal logoutresponse.response_id, logoutresponse.uuid - assert_equal "new_uuid", logoutresponse.response_id + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.certificate, settings.private_key = CertificateHelper.generate_pem_array(algorithm, not_after: Time.now - 60) + settings.security[:check_sp_cert_expiration] = true + + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + end + end end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 3a4bb1ca..a1e3bea8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -46,6 +46,23 @@ def fixture(document, base64 = true) end end + def self.with_each_key_algorithm(&block) + %i[rsa dsa ecdsa].each do |algorithm| + describe "#{algorithm.upcase} algorithm" do + block.call(algorithm) + end + end + end + + def signature_method(algorithm, digest = :sha256) + algorithm = :ecdsa if algorithm == :ec + RubySaml::XML::Crypto.const_get("#{algorithm}_#{digest}".upcase) + end + + def signature_method_matcher(algorithm, digest = :sha256) + %r{} + end + def read_response(response) File.read(File.join(File.dirname(__FILE__), "responses", response)) end diff --git a/test/utils_test.rb b/test/utils_test.rb index c3d64121..f76b303d 100644 --- a/test/utils_test.rb +++ b/test/utils_test.rb @@ -344,8 +344,8 @@ def result(duration, reference = 0) describe '.decrypt_multi' do let(:private_key) { ruby_saml_key } - let(:invalid_key1) { CertificateHelper.generate_key } - let(:invalid_key2) { CertificateHelper.generate_key } + let(:invalid_key1) { CertificateHelper.generate_private_key } + let(:invalid_key2) { CertificateHelper.generate_private_key } let(:settings) { RubySaml::Settings.new(:private_key => private_key.to_pem) } let(:response) { RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) } let(:encrypted) do @@ -357,11 +357,11 @@ def result(duration, reference = 0) end it 'successfully decrypts with the first private key' do - assert_match /\A RubySaml::XML::Document::SHA384) end it "validate using SHA512" do @@ -191,7 +191,7 @@ class XmlTest < Minitest::Test end end - describe "XmlSecurity::SignedDocument" do + describe "RubySaml::XML::SignedDocument" do describe "#extract_inclusive_namespaces" do it "support explicit namespace resolution for exclusive canonicalization" do