ruby-saml 0.9.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|