Skip to content

Commit 455d17d

Browse files
[READY] v2.0: Support DSA and ECDSA signing keys (#705)
* v2.0 Support EC/DSA crypto
1 parent fd86e2c commit 455d17d

32 files changed

+2003
-1338
lines changed

.rubocop_todo.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,12 @@ Metrics/CyclomaticComplexity:
204204
# Offense count: 58
205205
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
206206
Metrics/MethodLength:
207-
Max: 63
207+
Max: 80
208+
209+
# Offense count: 1
210+
# Configuration parameters: CountComments, CountAsOne.
211+
Metrics/ModuleLength:
212+
Max: 300
208213

209214
# Offense count: 1
210215
# Configuration parameters: CountComments, CountAsOne.

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ In the above there are a few assumptions, one being that `response.nameid` is an
176176
This is all handled with how you specify the settings that are in play via the `saml_settings` method.
177177
That could be implemented along the lines of this:
178178
179-
```
179+
```ruby
180180
response = RubySaml::Response.new(params[:SAMLResponse])
181181
response.settings = saml_settings
182182
```
@@ -759,6 +759,11 @@ Note the following:
759759
inactive/expired certificates. This avoids validation errors when the IdP reads the SP
760760
metadata.
761761
762+
#### Key Algorithm Support
763+
764+
Ruby SAML supports RSA, DSA, and ECDSA keys for both SP and IdP certificates.
765+
JRuby cannot support ECDSA due to a [known issue](https://github.com/jruby/jruby-openssl/issues/257).
766+
762767
#### Audience Validation
763768
764769
A service provider should only consider a SAML response valid if the IdP includes an <AudienceRestriction>

UPGRADING.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@
88

99
Before attempting to upgrade to `2.0.0`:
1010
- Upgrade your project to minimum Ruby 3.0, JRuby 9.4, or TruffleRuby 22.
11-
- Upgrade RubySaml to `1.17.x`. Note that RubySaml `1.17.x` is compatible with up to Ruby 3.3.
11+
- Upgrade RubySaml to `1.17.x`.
12+
- In RubySaml `1.17.x`, if you were using the SHA-1 default behavior, change your settings to use SHA-256 as per below:
13+
14+
```ruby
15+
# Set this in RubySaml 1.17.x, can be removed when upgrading to 2.0.0
16+
settings.idp_cert_fingerprint_algorithm = XMLSecurity::Document::SHA256
17+
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
18+
settings.security[:digest_method] = XMLSecurity::Document::SHA256
19+
```
1220

1321
### Root "OneLogin" namespace changed to "RubySaml"
1422

@@ -38,16 +46,17 @@ For security reasons, RubySaml version `2.0.0` uses SHA-256 as its default hashi
3846
instead of the now-obsolete SHA-1. This affects:
3947
- The default signature and digest algorithms used when generating SP metadata.
4048
- The default signature algorithm used when generating SP messages such as AuthnRequests.
41-
- The default fingerprint of IdP metadata (`:idp_cert_fingerprint` as generated by `RubySaml::IdpMetadataParser`)
49+
- The `:idp_cert_fingerprint` of IdP metadata as generated by `RubySaml::IdpMetadataParser`.
4250

43-
To preserve the old insecure SHA-1 behavior *(not recommended)*, you may set `RubySaml::Settings` as follows:
51+
If you see any signature or fingerprint mismatch errors after upgrading to RubySaml `2.0.0`,
52+
this change is likely the reason. To preserve the old insecure SHA-1 behavior *(not recommended)*,
53+
you may set `RubySaml::Settings` as follows:
4454

4555
```ruby
4656
# Preserve RubySaml 1.x insecure SHA-1 behavior
47-
settings = RubySaml::Settings.new
48-
settings.idp_cert_fingerprint_algorithm = RubySaml::XML::Document::SHA1
49-
settings.security[:digest_method] = RubySaml::XML::Document::SHA1
50-
settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1
57+
settings.idp_cert_fingerprint_algorithm = RubySaml::XML::Crypto::SHA1
58+
settings.security[:digest_method] = RubySaml::XML::Crypto::SHA1
59+
settings.security[:signature_method] = RubySaml::XML::Crypto::RSA_SHA1
5160
```
5261

5362
### Removal of embed_sign setting
@@ -94,12 +103,14 @@ The following parameters in `RubySaml::Settings` are deprecated and will be remo
94103

95104
### Minor changes to Util#format_cert and #format_private_key
96105

97-
Version 2.0.0 standardizes how RubySaml reads and formats certificate and private key
98-
PEM strings. In general, version 2.0.0 is more permissive than 1.x, and the changes
106+
107+
Version `2.0.0` standardizes how RubySaml reads and formats certificate and private key
108+
PEM strings. In general, version `2.0.0` is more permissive than `1.x`, and the changes
99109
are not anticipated to affect most users. Please note the change affects parameters
100110
such `#idp_cert` and `#certificate`, as well as the `RubySaml::Util#format_cert`
101111
and `#format_private_key` methods. Specifically:
102112

113+
103114
| # | Input value | RubySaml 2.0.0 | RubySaml 1.x |
104115
|---|------------------------------------------------------|---------------------------------------------------------|---------------------------|
105116
| 1 | Input contains a bad (e.g. non-base64) PEM | Skip PEM formatting | Return a bad PEM |
@@ -113,7 +124,7 @@ and `#format_private_key` methods. Specifically:
113124
**Notes**
114125
- Case 3: For example, `-----BEGIN TRUSTED X509 CERTIFICATE-----` is now
115126
considered a valid header as an input, but it will be formatted to
116-
`-----BEGIN CERTIFICATE-----` in the output. As a special case, in both 2.0.0
127+
`-----BEGIN CERTIFICATE-----` in the output. As a special case, in both `2.0.0`
117128
and 1.x, if `RSA PRIVATE KEY` is present in the input string, the `RSA` prefix will
118129
be preserved in the output.
119130
- Case 5: When formatting multiple certificates in one string (i.e. a certificate chain),

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/response.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -882,8 +882,8 @@ def validate_signature
882882

883883
if fingerprint && doc.validate_document(fingerprint, @soft, opts)
884884
if settings.security[:check_idp_cert_expiration] && RubySaml::Utils.is_cert_expired(idp_cert)
885-
error_msg = "IdP x509 certificate expired"
886-
return append_error(error_msg)
885+
error_msg = "IdP x509 certificate expired"
886+
return append_error(error_msg)
887887
end
888888
else
889889
return append_error(error_msg)

lib/ruby_saml/saml_message.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,7 @@ def decode(string)
133133
# @return [String] The encoded string
134134
#
135135
def encode(string)
136-
if Base64.respond_to?(:strict_encode64)
137-
Base64.strict_encode64(string)
138-
else
139-
Base64.encode64(string).gsub(/\n/, "")
140-
end
136+
Base64.strict_encode64(string)
141137
end
142138

143139
# Check if a string is base64 encoded

lib/ruby_saml/settings.rb

Lines changed: 40 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,43 @@ 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] || 'sha256'
279+
key_alg_fallback, hash_alg = sig_alg.to_s.match(/(?:\A|(rsa|ecdsa|ec|dsa)?[# _-])(sha\d+)\z/i)&.[](1..2)
280+
key_alg_real = 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+
end
285+
key_alg = key_alg_real || key_alg_fallback || 'RSA'
286+
key_alg = 'ECDSA' if key_alg.casecmp('EC') == 0
287+
288+
begin
289+
RubySaml::XML::Crypto.const_get("#{key_alg}_#{hash_alg}".upcase)
290+
rescue NameError
291+
raise ArgumentError.new("Unsupported signature method#{" for #{key_alg_real} key" if key_alg_real}: #{sig_alg}")
292+
end
293+
end
294+
295+
# @return [String] The XML Signature Digest attribute.
296+
def get_sp_digest_method
297+
digest_alg = security[:digest_method] || 'sha1' # TODO: change to sha256 by default
298+
alg = digest_alg.to_s.match(/(?:\A|#)(sha\d+)\z/i)[1]
299+
300+
begin
301+
RubySaml::XML::Crypto.const_get(alg.upcase)
302+
rescue NameError
303+
raise ArgumentError.new("Unsupported digest method: #{digest_alg}")
304+
end
305+
end
306+
270307
# @deprecated Will be removed in v2.1.0
271308
def certificate_new
272309
certificate_new_deprecation

0 commit comments

Comments
 (0)