Skip to content

Commit

Permalink
v2.0 Support EC/DSA crypto
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyshields committed Jul 10, 2024
1 parent 626140b commit f771caa
Show file tree
Hide file tree
Showing 25 changed files with 991 additions and 793 deletions.
6 changes: 3 additions & 3 deletions lib/ruby_saml/authrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_saml/idp_metadata_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_saml/logoutrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_saml/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
44 changes: 41 additions & 3 deletions lib/ruby_saml/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -159,7 +159,7 @@ def get_idp_cert_multi
certs
end

# @return [Hash<Symbol, Array<Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>>>]
# @return [Hash<Symbol, Array<Array<OpenSSL::X509::Certificate, OpenSSL::PKey::PKey>>>]
# 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.
Expand All @@ -179,7 +179,7 @@ def get_sp_certs
active_certs.freeze
end

# @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>]
# @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::PKey>]
# The SP signing certificate and private key.
def get_sp_signing_pair
get_sp_certs[:signing].first
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_saml/slo_logoutresponse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
32 changes: 22 additions & 10 deletions lib/ruby_saml/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,14 @@ def self.format_private_key(key)
# don't try to format an encoded private key or if is empty
return key if key.nil? || key.empty? || key.match(/\x0d/)

# is this an rsa key?
rsa_key = key.match("RSA PRIVATE KEY")
key = key.gsub(/-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?-{5}/, "")
key_algo = key.match(/((?:RSA|DSA|EC|ECDSA) )PRIVATE KEY/)&.[](1)
key = key.gsub(/-{5}\s?(BEGIN|END)( (?:RSA|DSA|EC|ECDSA))? PRIVATE KEY\s?-{5}/, "")
key = key.gsub(/\n/, "")
key = key.gsub(/\r/, "")
key = key.gsub(/\s/, "")
key = key.scan(/.{1,64}/)
key = key.join("\n")
key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
key_label = "#{key_algo}PRIVATE KEY"
"-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
end

Expand All @@ -149,12 +148,25 @@ def self.build_cert_object(cert)
# Given a private key string, return an OpenSSL::PKey::RSA object.
#
# @param cert [String] The original private key
# @return [OpenSSL::PKey::RSA] The private key object
# @return [OpenSSL::PKey::PKey] The private key object
#
def self.build_private_key_object(private_key)
return nil if private_key.nil? || private_key.empty?

OpenSSL::PKey::RSA.new(format_private_key(private_key))
private_key = format_private_key(private_key)
error = nil

[OpenSSL::PKey::RSA,
OpenSSL::PKey::DSA,
OpenSSL::PKey::EC].each do |key_class|
begin
return key_class.new(private_key)
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
Expand Down Expand Up @@ -236,7 +248,7 @@ def self.escape_request_param(param, lowercase_url_encoding)
#
def self.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

Expand Down Expand Up @@ -266,7 +278,7 @@ def self.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<OpenSSL::PKey::RSA>] The Service provider private key
# @param private_keys [Array<OpenSSL::PKey::PKey>] The Service provider private key
# @return [String] The decrypted data
def self.decrypt_multi(encrypted_node, private_keys)
raise ArgumentError.new('private_keys must be specified') if !private_keys || private_keys.empty?
Expand All @@ -285,7 +297,7 @@ def self.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 self.decrypt_data(encrypted_node, private_key)
encrypt_data = REXML::XPath.first(
Expand All @@ -311,7 +323,7 @@ def self.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 self.retrieve_symmetric_key(encrypt_data, private_key)
encrypted_key = REXML::XPath.first(
Expand Down
1 change: 1 addition & 0 deletions lib/ruby_saml/xml.rb
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
46 changes: 13 additions & 33 deletions lib/ruby_saml/xml/base_document.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading

0 comments on commit f771caa

Please sign in to comment.