Skip to content

Commit f771caa

Browse files
committed
v2.0 Support EC/DSA crypto
1 parent 626140b commit f771caa

25 files changed

Lines changed: 991 additions & 793 deletions

lib/ruby_saml/authrequest.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,14 @@ def create_params(settings, params={})
7777
sp_signing_key = settings.get_sp_signing_key
7878

7979
if binding_redirect && settings.security[:authn_requests_signed] && sp_signing_key
80-
params['SigAlg'] = settings.security[:signature_method]
80+
params['SigAlg'] = settings.get_sp_signature_method
8181
url_string = RubySaml::Utils.build_query(
8282
type: 'SAMLRequest',
8383
data: base64_request,
8484
relay_state: relay_state,
8585
sig_alg: params['SigAlg']
8686
)
87-
sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm(settings.security[:signature_method])
87+
sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method)
8888
signature = sp_signing_key.sign(sign_algorithm.new, url_string)
8989
params['Signature'] = encode(signature)
9090
end
@@ -185,7 +185,7 @@ def create_xml_document(settings)
185185
def sign_document(document, settings)
186186
cert, private_key = settings.get_sp_signing_pair
187187
if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && private_key && cert
188-
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
188+
document.sign_document(private_key, cert, settings.get_sp_signature_method, settings.get_sp_digest_method)
189189
end
190190

191191
document

lib/ruby_saml/idp_metadata_parser.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ def fingerprint(certificate, fingerprint_algorithm = RubySaml::XML::Document::SH
398398

399399
cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate))
400400

401-
fingerprint_alg = RubySaml::XML::BaseDocument.new.algorithm(fingerprint_algorithm).new
401+
fingerprint_alg = RubySaml::XML::Crypto.hash_algorithm(fingerprint_algorithm).new
402402
fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":")
403403
end
404404
end

lib/ruby_saml/logoutrequest.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ def create_params(settings, params={})
7575
sp_signing_key = settings.get_sp_signing_key
7676

7777
if binding_redirect && settings.security[:logout_requests_signed] && sp_signing_key
78-
params['SigAlg'] = settings.security[:signature_method]
78+
params['SigAlg'] = settings.get_sp_signature_method
7979
url_string = RubySaml::Utils.build_query(
8080
type: 'SAMLRequest',
8181
data: base64_request,
8282
relay_state: relay_state,
8383
sig_alg: params['SigAlg']
8484
)
85-
sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm(settings.security[:signature_method])
85+
sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method)
8686
signature = settings.get_sp_signing_key.sign(sign_algorithm.new, url_string)
8787
params['Signature'] = encode(signature)
8888
end
@@ -144,7 +144,7 @@ def sign_document(document, settings)
144144
# embed signature
145145
cert, private_key = settings.get_sp_signing_pair
146146
if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && private_key && cert
147-
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
147+
document.sign_document(private_key, cert, settings.get_sp_signature_method, settings.get_sp_digest_method)
148148
end
149149

150150
document

lib/ruby_saml/metadata.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def embed_signature(meta_doc, settings)
142142
cert, private_key = settings.get_sp_signing_pair
143143
return unless private_key && cert
144144

145-
meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
145+
meta_doc.sign_document(private_key, cert, settings.get_sp_signature_method, settings.get_sp_digest_method)
146146
end
147147

148148
def output_xml(meta_doc, pretty_print)

lib/ruby_saml/settings.rb

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def get_fingerprint
126126
idp_cert_fingerprint || begin
127127
idp_cert = get_idp_cert
128128
if idp_cert
129-
fingerprint_alg = RubySaml::XML::BaseDocument.new.algorithm(idp_cert_fingerprint_algorithm).new
129+
fingerprint_alg = RubySaml::XML::Crypto.hash_algorithm(idp_cert_fingerprint_algorithm).new
130130
fingerprint_alg.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":")
131131
end
132132
end
@@ -159,7 +159,7 @@ def get_idp_cert_multi
159159
certs
160160
end
161161

162-
# @return [Hash<Symbol, Array<Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>>>]
162+
# @return [Hash<Symbol, Array<Array<OpenSSL::X509::Certificate, OpenSSL::PKey::PKey>>>]
163163
# Build the SP certificates and private keys from the settings. If
164164
# check_sp_cert_expiration is true, only returns certificates and private keys
165165
# that are not expired.
@@ -179,7 +179,7 @@ def get_sp_certs
179179
active_certs.freeze
180180
end
181181

182-
# @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>]
182+
# @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::PKey>]
183183
# The SP signing certificate and private key.
184184
def get_sp_signing_pair
185185
get_sp_certs[:signing].first
@@ -267,6 +267,44 @@ def get_binding(value)
267267
end
268268
end
269269

270+
# @return [String] The XML Signature Algorithm attribute.
271+
#
272+
# This method is intentionally hacky for backwards compatibility of the
273+
# settings.security[:signature_method] parameter. Previously, this parameter
274+
# could have a value such as "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
275+
# which assumes the public key type RSA. To add support for DSA and ECDSA, we will now
276+
# ignore the "rsa-" prefix and only use the "sha256" hash algorithm component.
277+
def get_sp_signature_method
278+
sig_alg = security[:signature_method] || 'sha1' # TODO: change to sha256 by default
279+
hash_alg = sig_alg.to_s.match(/(?:\A|[#_-])(sha\d+)\z/i)[1]
280+
key_alg = case get_sp_signing_key
281+
when OpenSSL::PKey::RSA then 'RSA'
282+
when OpenSSL::PKey::DSA then 'DSA'
283+
when OpenSSL::PKey::EC then 'ECDSA'
284+
else # rubocop:disable Lint/DuplicateBranch
285+
# raise ArgumentError.new("Unsupported signing key type: #{get_sp_signing_key.class}")
286+
'RSA'
287+
end
288+
289+
begin
290+
RubySaml::XML::Crypto.const_get("#{key_alg}_#{hash_alg}".upcase)
291+
rescue NameError
292+
raise ArgumentError.new("Unsupported signature method: #{sig_alg}")
293+
end
294+
end
295+
296+
# @return [String] The XML Signature Digest attribute.
297+
def get_sp_digest_method
298+
digest_alg = security[:digest_method] || 'sha1' # TODO: change to sha256 by default
299+
alg = digest_alg.to_s.match(/(?:\A|#)(sha\d+)\z/i)[1]
300+
301+
begin
302+
RubySaml::XML::Crypto.const_get(alg.upcase)
303+
rescue NameError
304+
raise ArgumentError.new("Unsupported signature method: #{digest_alg}")
305+
end
306+
end
307+
270308
# @deprecated Will be removed in v2.1.0
271309
def certificate_new
272310
certificate_new_deprecation

lib/ruby_saml/slo_logoutresponse.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,14 @@ def create_params(settings, request_id = nil, logout_message = nil, params = {},
8484
sp_signing_key = settings.get_sp_signing_key
8585

8686
if binding_redirect && settings.security[:logout_responses_signed] && sp_signing_key
87-
params['SigAlg'] = settings.security[:signature_method]
87+
params['SigAlg'] = settings.get_sp_signature_method
8888
url_string = RubySaml::Utils.build_query(
8989
type: 'SAMLResponse',
9090
data: base64_response,
9191
relay_state: relay_state,
9292
sig_alg: params['SigAlg']
9393
)
94-
sign_algorithm = RubySaml::XML::BaseDocument.new.algorithm(settings.security[:signature_method])
94+
sign_algorithm = RubySaml::XML::Crypto.hash_algorithm(settings.get_sp_signature_method)
9595
signature = sp_signing_key.sign(sign_algorithm.new, url_string)
9696
params['Signature'] = encode(signature)
9797
end
@@ -155,7 +155,7 @@ def sign_document(document, settings)
155155
# embed signature
156156
cert, private_key = settings.get_sp_signing_pair
157157
if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && private_key && cert
158-
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
158+
document.sign_document(private_key, cert, settings.get_sp_signature_method, settings.get_sp_digest_method)
159159
end
160160

161161
document

lib/ruby_saml/utils.rb

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,14 @@ def self.format_private_key(key)
123123
# don't try to format an encoded private key or if is empty
124124
return key if key.nil? || key.empty? || key.match(/\x0d/)
125125

126-
# is this an rsa key?
127-
rsa_key = key.match("RSA PRIVATE KEY")
128-
key = key.gsub(/-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?-{5}/, "")
126+
key_algo = key.match(/((?:RSA|DSA|EC|ECDSA) )PRIVATE KEY/)&.[](1)
127+
key = key.gsub(/-{5}\s?(BEGIN|END)( (?:RSA|DSA|EC|ECDSA))? PRIVATE KEY\s?-{5}/, "")
129128
key = key.gsub(/\n/, "")
130129
key = key.gsub(/\r/, "")
131130
key = key.gsub(/\s/, "")
132131
key = key.scan(/.{1,64}/)
133132
key = key.join("\n")
134-
key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
133+
key_label = "#{key_algo}PRIVATE KEY"
135134
"-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
136135
end
137136

@@ -149,12 +148,25 @@ def self.build_cert_object(cert)
149148
# Given a private key string, return an OpenSSL::PKey::RSA object.
150149
#
151150
# @param cert [String] The original private key
152-
# @return [OpenSSL::PKey::RSA] The private key object
151+
# @return [OpenSSL::PKey::PKey] The private key object
153152
#
154153
def self.build_private_key_object(private_key)
155154
return nil if private_key.nil? || private_key.empty?
156155

157-
OpenSSL::PKey::RSA.new(format_private_key(private_key))
156+
private_key = format_private_key(private_key)
157+
error = nil
158+
159+
[OpenSSL::PKey::RSA,
160+
OpenSSL::PKey::DSA,
161+
OpenSSL::PKey::EC].each do |key_class|
162+
begin
163+
return key_class.new(private_key)
164+
rescue OpenSSL::PKey::PKeyError => e
165+
error ||= e
166+
end
167+
end
168+
169+
raise error
158170
end
159171

160172
# Build the Query String signature that will be used in the HTTP-Redirect binding
@@ -236,7 +248,7 @@ def self.escape_request_param(param, lowercase_url_encoding)
236248
#
237249
def self.verify_signature(params)
238250
cert, sig_alg, signature, query_string = %i[cert sig_alg signature query_string].map { |k| params[k]}
239-
signature_algorithm = RubySaml::XML::BaseDocument.new.algorithm(sig_alg)
251+
signature_algorithm = RubySaml::XML::Crypto.hash_algorithm(sig_alg)
240252
cert.public_key.verify(signature_algorithm.new, Base64.decode64(signature), query_string)
241253
end
242254

@@ -266,7 +278,7 @@ def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil
266278
# Obtains the decrypted string from an Encrypted node element in XML,
267279
# given multiple private keys to try.
268280
# @param encrypted_node [REXML::Element] The Encrypted element
269-
# @param private_keys [Array<OpenSSL::PKey::RSA>] The Service provider private key
281+
# @param private_keys [Array<OpenSSL::PKey::PKey>] The Service provider private key
270282
# @return [String] The decrypted data
271283
def self.decrypt_multi(encrypted_node, private_keys)
272284
raise ArgumentError.new('private_keys must be specified') if !private_keys || private_keys.empty?
@@ -285,7 +297,7 @@ def self.decrypt_multi(encrypted_node, private_keys)
285297

286298
# Obtains the decrypted string from an Encrypted node element in XML
287299
# @param encrypted_node [REXML::Element] The Encrypted element
288-
# @param private_key [OpenSSL::PKey::RSA] The Service provider private key
300+
# @param private_key [OpenSSL::PKey::PKey] The Service provider private key
289301
# @return [String] The decrypted data
290302
def self.decrypt_data(encrypted_node, private_key)
291303
encrypt_data = REXML::XPath.first(
@@ -311,7 +323,7 @@ def self.decrypt_data(encrypted_node, private_key)
311323

312324
# Obtains the symmetric key from the EncryptedData element
313325
# @param encrypt_data [REXML::Element] The EncryptedData element
314-
# @param private_key [OpenSSL::PKey::RSA] The Service provider private key
326+
# @param private_key [OpenSSL::PKey::PKey] The Service provider private key
315327
# @return [String] The symmetric key
316328
def self.retrieve_symmetric_key(encrypt_data, private_key)
317329
encrypted_key = REXML::XPath.first(

lib/ruby_saml/xml.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require 'ruby_saml/xml/crypto'
34
require 'ruby_saml/xml/base_document'
45
require 'ruby_saml/xml/document'
56
require 'ruby_saml/xml/signed_document'

lib/ruby_saml/xml/base_document.rb

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,35 @@
11
# frozen_string_literal: true
22

33
require 'rexml/document'
4+
require 'rexml/security'
45
require 'rexml/xpath'
56
require 'nokogiri'
67
require 'openssl'
78
require 'digest/sha1'
89
require 'digest/sha2'
10+
require 'ruby_saml/xml/crypto'
911

1012
module RubySaml
1113
module XML
1214
class BaseDocument < REXML::Document
15+
# TODO: This affects the global state
1316
REXML::Security.entity_expansion_limit = 0
1417

15-
C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#'
16-
DSIG = 'http://www.w3.org/2000/09/xmldsig#'
18+
# @deprecated Constants moved to Crypto module
19+
C14N = RubySaml::XML::Crypto::C14N
20+
DSIG = RubySaml::XML::Crypto::DSIG
21+
1722
NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
1823
Nokogiri::XML::ParseOptions::NONET
1924

20-
def canon_algorithm(element)
21-
algorithm = element
22-
if algorithm.is_a?(REXML::Element)
23-
algorithm = element.attribute('Algorithm').value
24-
end
25-
26-
case algorithm
27-
when 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315',
28-
'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments'
29-
Nokogiri::XML::XML_C14N_1_0
30-
when 'http://www.w3.org/2006/12/xml-c14n11',
31-
'http://www.w3.org/2006/12/xml-c14n11#WithComments'
32-
Nokogiri::XML::XML_C14N_1_1
33-
else
34-
Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
35-
end
25+
# @deprecated Remove in v2.1.0
26+
def canon_algorithm(algorithm)
27+
RubySaml::XML::Crypto.canon_algorithm(algorithm)
3628
end
3729

38-
def algorithm(element)
39-
algorithm = element
40-
if algorithm.is_a?(REXML::Element)
41-
algorithm = element.attribute('Algorithm').value
42-
end
43-
44-
algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && ::Regexp.last_match(2).to_i
45-
46-
case algorithm
47-
when 1 then OpenSSL::Digest::SHA1
48-
when 384 then OpenSSL::Digest::SHA384
49-
when 512 then OpenSSL::Digest::SHA512
50-
else
51-
OpenSSL::Digest::SHA256
52-
end
30+
# @deprecated Remove in v2.1.0
31+
def algorithm(algorithm)
32+
RubySaml::XML::Crypto.hash_algorithm(algorithm)
5333
end
5434
end
5535
end

0 commit comments

Comments
 (0)