ruby-saml 0.9.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of ruby-saml might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/LICENSE +1 -1
- data/README.md +71 -15
- data/changelog.md +15 -6
- data/lib/onelogin/ruby-saml.rb +1 -0
- data/lib/onelogin/ruby-saml/attribute_service.rb +25 -2
- data/lib/onelogin/ruby-saml/attributes.rb +42 -23
- data/lib/onelogin/ruby-saml/authrequest.rb +33 -8
- data/lib/onelogin/ruby-saml/http_error.rb +7 -0
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +65 -10
- data/lib/onelogin/ruby-saml/logging.rb +14 -10
- data/lib/onelogin/ruby-saml/logoutrequest.rb +39 -14
- data/lib/onelogin/ruby-saml/logoutresponse.rb +166 -39
- data/lib/onelogin/ruby-saml/metadata.rb +40 -23
- data/lib/onelogin/ruby-saml/response.rb +562 -88
- data/lib/onelogin/ruby-saml/saml_message.rb +80 -14
- data/lib/onelogin/ruby-saml/settings.rb +62 -23
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +210 -20
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +44 -13
- data/lib/onelogin/ruby-saml/utils.rb +163 -40
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/schemas/saml-schema-metadata-2.0.xsd +0 -2
- data/lib/xml_security.rb +87 -29
- data/ruby-saml.gemspec +1 -0
- data/test/certificates/{r1_certificate2_base64 → certificate_without_head_foot} +0 -0
- data/test/certificates/formatted_certificate +14 -0
- data/test/certificates/formatted_private_key +12 -0
- data/test/certificates/formatted_rsa_private_key +12 -0
- data/test/certificates/invalid_certificate1 +1 -0
- data/test/certificates/invalid_certificate2 +1 -0
- data/test/certificates/invalid_certificate3 +12 -0
- data/test/certificates/invalid_private_key1 +1 -0
- data/test/certificates/invalid_private_key2 +1 -0
- data/test/certificates/invalid_private_key3 +10 -0
- data/test/certificates/invalid_rsa_private_key1 +1 -0
- data/test/certificates/invalid_rsa_private_key2 +1 -0
- data/test/certificates/invalid_rsa_private_key3 +10 -0
- data/test/idp_metadata_parser_test.rb +41 -4
- data/test/logging_test.rb +62 -0
- data/test/logout_requests/invalid_slo_request.xml +6 -0
- data/test/{responses → logout_requests}/slo_request.xml +0 -0
- data/test/logout_requests/slo_request.xml.base64 +1 -0
- data/test/logout_requests/slo_request_deflated.xml.base64 +1 -0
- data/test/logout_requests/slo_request_with_session_index.xml +5 -0
- data/test/{responses → logout_responses}/logoutresponse_fixtures.rb +6 -6
- data/test/logoutrequest_test.rb +79 -52
- data/test/logoutresponse_test.rb +206 -59
- data/test/metadata_test.rb +77 -7
- data/test/request_test.rb +80 -65
- data/test/response_test.rb +862 -189
- data/test/responses/attackxee.xml +13 -0
- data/test/responses/invalids/invalid_audience.xml.base64 +1 -0
- data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
- data/test/responses/invalids/invalid_signature_position.xml.base64 +1 -0
- data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +1 -0
- data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +1 -0
- data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +1 -0
- data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +1 -0
- data/test/responses/invalids/multiple_assertions.xml.base64 +2 -0
- data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
- data/test/responses/invalids/no_id.xml.base64 +1 -0
- data/test/responses/invalids/no_saml2.xml.base64 +1 -0
- data/test/responses/invalids/no_signature.xml.base64 +1 -0
- data/test/responses/invalids/no_status.xml.base64 +1 -0
- data/test/responses/invalids/no_status_code.xml.base64 +1 -0
- data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +1 -0
- data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +1 -0
- data/test/responses/invalids/response_encrypted_attrs.xml.base64 +1 -0
- data/test/responses/invalids/response_invalid_signed_element.xml.base64 +1 -0
- data/test/responses/invalids/status_code_responder.xml.base64 +1 -0
- data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +1 -0
- data/test/responses/{response4.xml.base64 → response_assertion_wrapped.xml.base64} +0 -0
- data/test/responses/response_encrypted_nameid.xml.base64 +1 -0
- data/test/responses/response_unsigned_xml_base64 +1 -0
- data/test/responses/{response5.xml.base64 → response_with_saml2_namespace.xml.base64} +0 -0
- data/test/responses/{response3.xml.base64 → response_with_signed_assertion.xml.base64} +0 -0
- data/test/responses/{r1_response6.xml.base64 → response_with_signed_assertion_2.xml.base64} +0 -0
- data/test/responses/{response1.xml.base64 → response_with_undefined_recipient.xml.base64} +0 -0
- data/test/responses/{response2.xml.base64 → response_without_attributes.xml.base64} +0 -0
- data/test/responses/{wrapped_response_2.xml.base64 → response_wrapped.xml.base64} +0 -0
- data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +1 -0
- data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +1 -0
- data/test/responses/valid_response.xml.base64 +1 -0
- data/test/saml_message_test.rb +56 -0
- data/test/settings_test.rb +138 -1
- data/test/slo_logoutrequest_test.rb +239 -28
- data/test/slo_logoutresponse_test.rb +93 -71
- data/test/test_helper.rb +138 -31
- data/test/utils_test.rb +129 -25
- data/test/xml_security_test.rb +140 -71
- metadata +142 -25
- data/test/responses/response_node_text_attack.xml.base64 +0 -1
@@ -3,16 +3,31 @@ require "uuid"
|
|
3
3
|
require "onelogin/ruby-saml/logging"
|
4
4
|
require "onelogin/ruby-saml/saml_message"
|
5
5
|
|
6
|
+
# Only supports SAML 2.0
|
6
7
|
module OneLogin
|
7
8
|
module RubySaml
|
9
|
+
|
10
|
+
# SAML2 Logout Response (SLO SP initiated, Parser)
|
11
|
+
#
|
8
12
|
class SloLogoutresponse < SamlMessage
|
9
13
|
|
10
|
-
|
14
|
+
# Logout Response ID
|
15
|
+
attr_reader :uuid
|
11
16
|
|
17
|
+
# Initializes the Logout Response. A SloLogoutresponse Object that is an extension of the SamlMessage class.
|
18
|
+
# Asigns an ID, a random uuid.
|
19
|
+
#
|
12
20
|
def initialize
|
13
21
|
@uuid = "_" + UUID.new.generate
|
14
22
|
end
|
15
23
|
|
24
|
+
# Creates the Logout Response string.
|
25
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
26
|
+
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
27
|
+
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
28
|
+
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
29
|
+
# @return [String] Logout Request string that includes the SAMLRequest
|
30
|
+
#
|
16
31
|
def create(settings, request_id = nil, logout_message = nil, params = {})
|
17
32
|
params = create_params(settings, request_id, logout_message, params)
|
18
33
|
params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
|
@@ -25,6 +40,13 @@ module OneLogin
|
|
25
40
|
@logout_url = settings.idp_slo_target_url + response_params
|
26
41
|
end
|
27
42
|
|
43
|
+
# Creates the Get parameters for the logout response.
|
44
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
45
|
+
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
46
|
+
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
47
|
+
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
48
|
+
# @return [Hash] Parameters
|
49
|
+
#
|
28
50
|
def create_params(settings, request_id = nil, logout_message = nil, params = {})
|
29
51
|
# The method expects :RelayState but sometimes we get 'RelayState' instead.
|
30
52
|
# Based on the HashWithIndifferentAccess value in Rails we could experience
|
@@ -45,11 +67,14 @@ module OneLogin
|
|
45
67
|
|
46
68
|
if settings.security[:logout_responses_signed] && !settings.security[:embed_sign] && settings.private_key
|
47
69
|
params['SigAlg'] = settings.security[:signature_method]
|
48
|
-
url_string
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
70
|
+
url_string = OneLogin::RubySaml::Utils.build_query(
|
71
|
+
:type => 'SAMLResponse',
|
72
|
+
:data => base64_response,
|
73
|
+
:relay_state => relay_state,
|
74
|
+
:sig_alg => params['SigAlg']
|
75
|
+
)
|
76
|
+
sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
|
77
|
+
signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
|
53
78
|
params['Signature'] = encode(signature)
|
54
79
|
end
|
55
80
|
|
@@ -60,6 +85,12 @@ module OneLogin
|
|
60
85
|
response_params
|
61
86
|
end
|
62
87
|
|
88
|
+
# Creates the SAMLResponse String.
|
89
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
90
|
+
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
91
|
+
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
92
|
+
# @return [String] The SAMLResponse String.
|
93
|
+
#
|
63
94
|
def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil)
|
64
95
|
time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
65
96
|
|
@@ -73,6 +104,11 @@ module OneLogin
|
|
73
104
|
root.attributes['InResponseTo'] = request_id unless request_id.nil?
|
74
105
|
root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
|
75
106
|
|
107
|
+
if settings.issuer != nil
|
108
|
+
issuer = root.add_element "saml:Issuer"
|
109
|
+
issuer.text = settings.issuer
|
110
|
+
end
|
111
|
+
|
76
112
|
# add success message
|
77
113
|
status = root.add_element 'samlp:Status'
|
78
114
|
|
@@ -85,15 +121,10 @@ module OneLogin
|
|
85
121
|
status_message = status.add_element 'samlp:StatusMessage'
|
86
122
|
status_message.text = logout_message
|
87
123
|
|
88
|
-
if settings.issuer != nil
|
89
|
-
issuer = root.add_element "saml:Issuer"
|
90
|
-
issuer.text = settings.issuer
|
91
|
-
end
|
92
|
-
|
93
124
|
# embed signature
|
94
125
|
if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
|
95
|
-
private_key = settings.get_sp_key
|
96
|
-
cert = settings.get_sp_cert
|
126
|
+
private_key = settings.get_sp_key
|
127
|
+
cert = settings.get_sp_cert
|
97
128
|
response_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
98
129
|
end
|
99
130
|
|
@@ -1,49 +1,172 @@
|
|
1
1
|
module OneLogin
|
2
2
|
module RubySaml
|
3
|
+
|
4
|
+
# SAML2 Auxiliary class
|
5
|
+
#
|
3
6
|
class Utils
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
|
8
|
+
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
9
|
+
XENC = "http://www.w3.org/2001/04/xmlenc#"
|
10
|
+
|
11
|
+
# Return a properly formatted x509 certificate
|
12
|
+
#
|
13
|
+
# @param cert [String] The original certificate
|
14
|
+
# @return [String] The formatted certificate
|
15
|
+
#
|
16
|
+
def self.format_cert(cert)
|
17
|
+
# don't try to format an encoded certificate or if is empty or nil
|
18
|
+
return cert if cert.nil? || cert.empty? || cert.match(/\x0d/)
|
19
|
+
|
20
|
+
cert = cert.gsub(/\-{5}\s?(BEGIN|END) CERTIFICATE\s?\-{5}/, "")
|
21
|
+
cert = cert.gsub(/[\n\r\s]/, "")
|
22
|
+
cert = cert.scan(/.{1,64}/)
|
23
|
+
cert = cert.join("\n")
|
24
|
+
"-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return a properly formatted private key
|
28
|
+
#
|
29
|
+
# @param key [String] The original private key
|
30
|
+
# @return [String] The formatted private key
|
31
|
+
#
|
32
|
+
def self.format_private_key(key)
|
33
|
+
# don't try to format an encoded private key or if is empty
|
34
|
+
return key if key.nil? || key.empty? || key.match(/\x0d/)
|
35
|
+
|
36
|
+
# is this an rsa key?
|
37
|
+
rsa_key = key.match("RSA PRIVATE KEY")
|
38
|
+
key = key.gsub(/\-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?\-{5}/, "")
|
39
|
+
key = key.gsub(/[\n\r\s]/, "")
|
40
|
+
key = key.scan(/.{1,64}/)
|
41
|
+
key = key.join("\n")
|
42
|
+
key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
|
43
|
+
"-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Build the Query String signature that will be used in the HTTP-Redirect binding
|
47
|
+
# to generate the Signature
|
48
|
+
# @param params [Hash] Parameters to build the Query String
|
49
|
+
# @option params [String] :type 'SAMLRequest' or 'SAMLResponse'
|
50
|
+
# @option params [String] :data Base64 encoded SAMLRequest or SAMLResponse
|
51
|
+
# @option params [String] :relay_state The RelayState parameter
|
52
|
+
# @option params [String] :sig_alg The SigAlg parameter
|
53
|
+
# @return [String] The Query String
|
54
|
+
#
|
55
|
+
def self.build_query(params)
|
56
|
+
type, data, relay_state, sig_alg = [:type, :data, :relay_state, :sig_alg].map { |k| params[k]}
|
57
|
+
|
58
|
+
url_string = "#{type}=#{CGI.escape(data)}"
|
59
|
+
url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
|
60
|
+
url_string << "&SigAlg=#{CGI.escape(sig_alg)}"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Validate the Signature parameter sent on the HTTP-Redirect binding
|
64
|
+
# @param params [Hash] Parameters to be used in the validation process
|
65
|
+
# @option params [OpenSSL::X509::Certificate] cert The Identity provider public certtificate
|
66
|
+
# @option params [String] sig_alg The SigAlg parameter
|
67
|
+
# @option params [String] signature The Signature parameter (base64 encoded)
|
68
|
+
# @option params [String] query_string The SigAlg parameter
|
69
|
+
# @return [Boolean] True if the Signature is valid, False otherwise
|
70
|
+
#
|
71
|
+
def self.verify_signature(params)
|
72
|
+
cert, sig_alg, signature, query_string = [:cert, :sig_alg, :signature, :query_string].map { |k| params[k]}
|
73
|
+
signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(sig_alg)
|
74
|
+
return cert.public_key.verify(signature_algorithm.new, Base64.decode64(signature), query_string)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Build the status error message
|
78
|
+
# @param status_code [String] StatusCode value
|
79
|
+
# @param status_message [Strig] StatusMessage value
|
80
|
+
# @return [String] The status error message
|
81
|
+
def self.status_error_msg(error_msg, status_code = nil, status_message = nil)
|
82
|
+
unless status_code.nil?
|
83
|
+
printable_code = status_code.split(':').last
|
84
|
+
error_msg << ', was ' + printable_code
|
15
85
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
def self.format_private_key(key, heads=true)
|
20
|
-
key = key.delete("\n").delete("\r").delete("\x0D")
|
21
|
-
if key
|
22
|
-
if key.index('-----BEGIN PRIVATE KEY-----') != nil
|
23
|
-
key = key.gsub('-----BEGIN PRIVATE KEY-----', '')
|
24
|
-
key = key.gsub('-----END PRIVATE KEY-----', '')
|
25
|
-
key = key.gsub(' ', '')
|
26
|
-
if heads
|
27
|
-
key = key.scan(/.{1,64}/).join("\n")+"\n"
|
28
|
-
key = "-----BEGIN PRIVATE KEY-----\n" + key + "-----END PRIVATE KEY-----\n"
|
29
|
-
end
|
30
|
-
else
|
31
|
-
key = key.gsub('-----BEGIN RSA PRIVATE KEY-----', '')
|
32
|
-
key = key.gsub('-----END RSA PRIVATE KEY-----', '')
|
33
|
-
key = key.gsub(' ', '')
|
34
|
-
if heads
|
35
|
-
key = key.scan(/.{1,64}/).join("\n")+"\n"
|
36
|
-
key = "-----BEGIN RSA PRIVATE KEY-----\n" + key + "-----END RSA PRIVATE KEY-----\n"
|
37
|
-
end
|
38
|
-
end
|
86
|
+
|
87
|
+
unless status_message.nil?
|
88
|
+
error_msg << ' -> ' + status_message
|
39
89
|
end
|
90
|
+
|
91
|
+
error_msg
|
92
|
+
end
|
93
|
+
|
94
|
+
# Obtains the decrypted string from an Encrypted node element in XML
|
95
|
+
# @param encrypted_node [REXML::Element] The Encrypted element
|
96
|
+
# @param private_key [OpenSSL::PKey::RSA] The Service provider private key
|
97
|
+
# @return [String] The decrypted data
|
98
|
+
def self.decrypt_data(encrypted_node, private_key)
|
99
|
+
encrypt_data = REXML::XPath.first(
|
100
|
+
encrypted_node,
|
101
|
+
"./xenc:EncryptedData",
|
102
|
+
{ 'xenc' => XENC }
|
103
|
+
)
|
104
|
+
symmetric_key = retrieve_symmetric_key(encrypt_data, private_key)
|
105
|
+
cipher_value = REXML::XPath.first(
|
106
|
+
encrypt_data,
|
107
|
+
"//xenc:EncryptedData/xenc:CipherData/xenc:CipherValue",
|
108
|
+
{ 'xenc' => XENC }
|
109
|
+
)
|
110
|
+
node = Base64.decode64(cipher_value.text)
|
111
|
+
encrypt_method = REXML::XPath.first(
|
112
|
+
encrypt_data,
|
113
|
+
"//xenc:EncryptedData/xenc:EncryptionMethod",
|
114
|
+
{ 'xenc' => XENC }
|
115
|
+
)
|
116
|
+
algorithm = encrypt_method.attributes['Algorithm']
|
117
|
+
retrieve_plaintext(node, symmetric_key, algorithm)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Obtains the symmetric key from the EncryptedData element
|
121
|
+
# @param encrypt_data [REXML::Element] The EncryptedData element
|
122
|
+
# @param private_key [OpenSSL::PKey::RSA] The Service provider private key
|
123
|
+
# @return [String] The symmetric key
|
124
|
+
def self.retrieve_symmetric_key(encrypt_data, private_key)
|
125
|
+
encrypted_symmetric_key_element = REXML::XPath.first(
|
126
|
+
encrypt_data,
|
127
|
+
"//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:CipherData/xenc:CipherValue",
|
128
|
+
{ "ds" => DSIG, "xenc" => XENC }
|
129
|
+
)
|
130
|
+
cipher_text = Base64.decode64(encrypted_symmetric_key_element.text)
|
131
|
+
encrypt_method = REXML::XPath.first(
|
132
|
+
encrypt_data,
|
133
|
+
"//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod",
|
134
|
+
{"ds" => DSIG, "xenc" => XENC }
|
135
|
+
)
|
136
|
+
algorithm = encrypt_method.attributes['Algorithm']
|
137
|
+
retrieve_plaintext(cipher_text, private_key, algorithm)
|
40
138
|
end
|
41
|
-
|
42
|
-
#
|
43
|
-
#
|
44
|
-
|
45
|
-
|
139
|
+
|
140
|
+
# Obtains the deciphered text
|
141
|
+
# @param cipher_text [String] The ciphered text
|
142
|
+
# @param symmetric_key [String] The symetric key used to encrypt the text
|
143
|
+
# @param algorithm [String] The encrypted algorithm
|
144
|
+
# @return [String] The deciphered text
|
145
|
+
def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm)
|
146
|
+
case algorithm
|
147
|
+
when 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' then cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').decrypt
|
148
|
+
when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt
|
149
|
+
when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt
|
150
|
+
when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
|
151
|
+
when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key
|
152
|
+
when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key
|
153
|
+
end
|
154
|
+
|
155
|
+
if cipher
|
156
|
+
iv_len = cipher.iv_len
|
157
|
+
data = cipher_text[iv_len..-1]
|
158
|
+
cipher.padding, cipher.key, cipher.iv = 0, symmetric_key, cipher_text[0..iv_len-1]
|
159
|
+
assertion_plaintext = cipher.update(data)
|
160
|
+
assertion_plaintext << cipher.final
|
161
|
+
elsif rsa
|
162
|
+
rsa.private_decrypt(cipher_text)
|
163
|
+
elsif oaep
|
164
|
+
oaep.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
165
|
+
else
|
166
|
+
cipher_text
|
167
|
+
end
|
46
168
|
end
|
169
|
+
|
47
170
|
end
|
48
171
|
end
|
49
|
-
end
|
172
|
+
end
|
@@ -247,8 +247,6 @@
|
|
247
247
|
<sequence>
|
248
248
|
<element ref="md:AssertionConsumerService" maxOccurs="unbounded"/>
|
249
249
|
<element ref="md:AttributeConsumingService" minOccurs="0" maxOccurs="unbounded"/>
|
250
|
-
<element ref="md:SingleLogoutService" minOccurs="0" maxOccurs="unbounded"/>
|
251
|
-
<element ref="md:KeyDescriptor" minOccurs="0" maxOccurs="unbounded"/>
|
252
250
|
</sequence>
|
253
251
|
<attribute name="AuthnRequestsSigned" type="boolean" use="optional"/>
|
254
252
|
<attribute name="WantAssertionsSigned" type="boolean" use="optional"/>
|
data/lib/xml_security.rb
CHANGED
@@ -29,15 +29,17 @@ require "openssl"
|
|
29
29
|
require 'nokogiri'
|
30
30
|
require "digest/sha1"
|
31
31
|
require "digest/sha2"
|
32
|
-
require "onelogin/ruby-saml/utils"
|
33
32
|
require "onelogin/ruby-saml/validation_error"
|
34
33
|
|
35
34
|
module XMLSecurity
|
36
35
|
|
37
36
|
class BaseDocument < REXML::Document
|
37
|
+
REXML::Document::entity_expansion_limit = 0
|
38
38
|
|
39
39
|
C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
|
40
40
|
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
41
|
+
NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
|
42
|
+
Nokogiri::XML::ParseOptions::NONET
|
41
43
|
|
42
44
|
def canon_algorithm(element)
|
43
45
|
algorithm = element
|
@@ -46,7 +48,6 @@ module XMLSecurity
|
|
46
48
|
end
|
47
49
|
|
48
50
|
case algorithm
|
49
|
-
when "http://www.w3.org/2001/10/xml-exc-c14n#" then Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
50
51
|
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0
|
51
52
|
when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1
|
52
53
|
else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
@@ -108,7 +109,9 @@ module XMLSecurity
|
|
108
109
|
#<Object />
|
109
110
|
#</Signature>
|
110
111
|
def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
|
111
|
-
noko = Nokogiri.parse(self.to_s)
|
112
|
+
noko = Nokogiri.parse(self.to_s) do |options|
|
113
|
+
options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
114
|
+
end
|
112
115
|
|
113
116
|
signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
|
114
117
|
signed_info_element = signature_element.add_element("ds:SignedInfo")
|
@@ -130,7 +133,10 @@ module XMLSecurity
|
|
130
133
|
reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element))
|
131
134
|
|
132
135
|
# add SignatureValue
|
133
|
-
noko_sig_element = Nokogiri.parse(signature_element.to_s)
|
136
|
+
noko_sig_element = Nokogiri.parse(signature_element.to_s) do |options|
|
137
|
+
options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
138
|
+
end
|
139
|
+
|
134
140
|
noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
|
135
141
|
canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
|
136
142
|
|
@@ -180,12 +186,19 @@ module XMLSecurity
|
|
180
186
|
def initialize(response, errors = [])
|
181
187
|
super(response)
|
182
188
|
@errors = errors
|
183
|
-
|
189
|
+
end
|
190
|
+
|
191
|
+
def signed_element_id
|
192
|
+
@signed_element_id ||= extract_signed_element_id
|
184
193
|
end
|
185
194
|
|
186
195
|
def validate_document(idp_cert_fingerprint, soft = true, options = {})
|
187
196
|
# get cert from response
|
188
|
-
cert_element = REXML::XPath.first(
|
197
|
+
cert_element = REXML::XPath.first(
|
198
|
+
self,
|
199
|
+
"//ds:X509Certificate",
|
200
|
+
{ "ds"=>DSIG }
|
201
|
+
)
|
189
202
|
unless cert_element
|
190
203
|
if soft
|
191
204
|
return false
|
@@ -193,7 +206,7 @@ module XMLSecurity
|
|
193
206
|
raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)")
|
194
207
|
end
|
195
208
|
end
|
196
|
-
base64_cert =
|
209
|
+
base64_cert = cert_element.text
|
197
210
|
cert_text = Base64.decode64(base64_cert)
|
198
211
|
cert = OpenSSL::X509::Certificate.new(cert_text)
|
199
212
|
|
@@ -219,37 +232,63 @@ module XMLSecurity
|
|
219
232
|
# check for inclusive namespaces
|
220
233
|
inclusive_namespaces = extract_inclusive_namespaces
|
221
234
|
|
222
|
-
document = Nokogiri.parse(self.to_s)
|
235
|
+
document = Nokogiri.parse(self.to_s) do |options|
|
236
|
+
options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
237
|
+
end
|
223
238
|
|
224
239
|
# create a working copy so we don't modify the original
|
225
240
|
@working_copy ||= REXML::Document.new(self.to_s).root
|
226
241
|
|
227
242
|
# store and remove signature node
|
228
243
|
@sig_element ||= begin
|
229
|
-
element = REXML::XPath.first(
|
244
|
+
element = REXML::XPath.first(
|
245
|
+
@working_copy,
|
246
|
+
"//ds:Signature",
|
247
|
+
{"ds"=>DSIG}
|
248
|
+
)
|
230
249
|
element.remove
|
231
250
|
end
|
232
251
|
|
233
252
|
# verify signature
|
234
|
-
signed_info_element
|
253
|
+
signed_info_element = REXML::XPath.first(
|
254
|
+
@sig_element,
|
255
|
+
"//ds:SignedInfo",
|
256
|
+
{"ds"=>DSIG}
|
257
|
+
)
|
235
258
|
noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
|
236
259
|
noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
|
237
|
-
canon_algorithm = canon_algorithm REXML::XPath.first(
|
260
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(
|
261
|
+
@sig_element,
|
262
|
+
'//ds:CanonicalizationMethod',
|
263
|
+
'ds' => DSIG
|
264
|
+
)
|
238
265
|
canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
|
239
266
|
noko_sig_element.remove
|
240
267
|
|
241
268
|
# check digests
|
242
269
|
REXML::XPath.each(@sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref|
|
243
|
-
uri
|
244
|
-
|
245
|
-
hashed_element
|
246
|
-
canon_algorithm
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
270
|
+
uri = ref.attributes.get_attribute("URI").value
|
271
|
+
|
272
|
+
hashed_element = document.at_xpath("//*[@ID=$uri]", nil, { 'uri' => uri[1..-1] })
|
273
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(
|
274
|
+
ref,
|
275
|
+
'//ds:CanonicalizationMethod',
|
276
|
+
{ "ds" => DSIG }
|
277
|
+
)
|
278
|
+
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
279
|
+
|
280
|
+
digest_algorithm = algorithm(REXML::XPath.first(
|
281
|
+
ref,
|
282
|
+
"//ds:DigestMethod",
|
283
|
+
{ "ds" => DSIG }
|
284
|
+
))
|
285
|
+
hash = digest_algorithm.digest(canon_hashed_element)
|
286
|
+
encoded_digest_value = REXML::XPath.first(
|
287
|
+
ref,
|
288
|
+
"//ds:DigestValue",
|
289
|
+
{ "ds" => DSIG }
|
290
|
+
).text
|
291
|
+
digest_value = Base64.decode64(encoded_digest_value)
|
253
292
|
|
254
293
|
unless digests_match?(hash, digest_value)
|
255
294
|
@errors << "Digest mismatch"
|
@@ -257,15 +296,25 @@ module XMLSecurity
|
|
257
296
|
end
|
258
297
|
end
|
259
298
|
|
260
|
-
base64_signature
|
261
|
-
|
299
|
+
base64_signature = REXML::XPath.first(
|
300
|
+
@sig_element,
|
301
|
+
"//ds:SignatureValue",
|
302
|
+
{"ds" => DSIG}
|
303
|
+
).text
|
304
|
+
|
305
|
+
signature = Base64.decode64(base64_signature)
|
262
306
|
|
263
307
|
# get certificate object
|
264
|
-
cert_text
|
265
|
-
cert
|
308
|
+
cert_text = Base64.decode64(base64_cert)
|
309
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
266
310
|
|
267
311
|
# signature method
|
268
|
-
|
312
|
+
sig_alg_value = REXML::XPath.first(
|
313
|
+
signed_info_element,
|
314
|
+
"//ds:SignatureMethod",
|
315
|
+
{"ds"=>DSIG}
|
316
|
+
)
|
317
|
+
signature_algorithm = algorithm(sig_alg_value)
|
269
318
|
|
270
319
|
unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
|
271
320
|
@errors << "Key validation error"
|
@@ -282,12 +331,21 @@ module XMLSecurity
|
|
282
331
|
end
|
283
332
|
|
284
333
|
def extract_signed_element_id
|
285
|
-
reference_element
|
286
|
-
|
334
|
+
reference_element = REXML::XPath.first(
|
335
|
+
self,
|
336
|
+
"//ds:Signature/ds:SignedInfo/ds:Reference",
|
337
|
+
{"ds"=>DSIG}
|
338
|
+
)
|
339
|
+
self.signed_element_id = reference_element.attribute("URI").value[1..-1] unless reference_element.nil?
|
287
340
|
end
|
288
341
|
|
289
342
|
def extract_inclusive_namespaces
|
290
|
-
|
343
|
+
element = REXML::XPath.first(
|
344
|
+
self,
|
345
|
+
"//ec:InclusiveNamespaces",
|
346
|
+
{ "ec" => C14N }
|
347
|
+
)
|
348
|
+
if element
|
291
349
|
prefix_list = element.attributes.get_attribute("PrefixList").value
|
292
350
|
prefix_list.split(" ")
|
293
351
|
else
|