Skip to content

Commit e01d4d5

Browse files
committed
Add EncryptedAttribute support. Found an issue related to the decrypt process, I fixed for the case of decrypt EncryptedAttribute
but fails when the EncryptedKey is not inside EncryptedData (there is a test that fails).
1 parent 20489a0 commit e01d4d5

5 files changed

Lines changed: 68 additions & 66 deletions

File tree

lib/onelogin/ruby-saml/response.rb

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,14 @@ def attributes
123123
stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement')
124124
stmt_elements.each do |stmt_element|
125125
stmt_element.elements.each do |attr_element|
126-
name = attr_element.attributes["Name"]
127-
values = attr_element.elements.collect{|e|
126+
if attr_element.name == "EncryptedAttribute"
127+
node = decrypt_attribute(attr_element.dup)
128+
else
129+
node = attr_element
130+
end
131+
132+
name = node.attributes["Name"]
133+
values = node.elements.collect{|e|
128134
if (e.elements.nil? || e.elements.size == 0)
129135
# SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
130136
# otherwise the value is to be regarded as empty.
@@ -297,7 +303,6 @@ def validate(collect_errors = false)
297303
:validate_id,
298304
:validate_success_status,
299305
:validate_num_assertion,
300-
:validate_no_encrypted_attributes,
301306
:validate_signed_elements,
302307
:validate_structure,
303308
:validate_in_response_to,
@@ -413,21 +418,6 @@ def validate_num_assertion
413418
true
414419
end
415420

416-
# Validates that there are not EncryptedAttribute (not supported)
417-
# If fails, the error is added to the errors array
418-
# @return [Boolean] True if there are no EncryptedAttribute elements, otherwise False if soft=True
419-
# @raise [ValidationError] if soft == false and validation fails
420-
#
421-
def validate_no_encrypted_attributes
422-
nodes = xpath_from_signed_assertion("/a:AttributeStatement/a:EncryptedAttribute")
423-
if nodes && nodes.length > 0
424-
return append_error("There is an EncryptedAttribute in the Response and this SP not support them")
425-
end
426-
427-
true
428-
end
429-
430-
431421
# Validates the Signed elements
432422
# If fails, the error is added to the errors array
433423
# @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
@@ -806,22 +796,39 @@ def decrypt_nameid(encryptedid_node)
806796
decrypt_element(encryptedid_node, /(.*<\/(\w+:)?NameID>)/m)
807797
end
808798

799+
# Decrypts an EncryptedID element
800+
# @param encryptedid_node [REXML::Element] The EncryptedID element
801+
# @return [REXML::Document] The decrypted EncrypedtID element
802+
#
803+
def decrypt_attribute(encryptedattribute_node)
804+
decrypt_element(encryptedattribute_node, /(.*<\/(\w+:)?Attribute>)/m)
805+
end
806+
809807
# Decrypt an element
810808
# @param encryptedid_node [REXML::Element] The encrypted element
809+
# @param rgrex string Regex
811810
# @return [REXML::Document] The decrypted element
812811
#
813812
def decrypt_element(encrypt_node, rgrex)
814813
if settings.nil? || !settings.get_sp_key
815814
raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it')
816815
end
817816

817+
818+
if encrypt_node.name == 'EncryptedAttribute'
819+
node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
820+
else
821+
node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'
822+
end
823+
818824
elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key)
819825
# If we get some problematic noise in the plaintext after decrypting.
820826
# This quick regexp parse will grab only the Element and discard the noise.
821827
elem_plaintext = elem_plaintext.match(rgrex)[0]
822-
# To avoid namespace errors if saml namespace is not defined at assertion_plaintext
823-
# create a parent node first with the saml namespace defined
824-
elem_plaintext = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + elem_plaintext + '</node>'
828+
829+
# To avoid namespace errors if saml namespace is not defined
830+
# create a parent node first with the namespace defined
831+
elem_plaintext = node_header + elem_plaintext + '</node>'
825832
doc = REXML::Document.new(elem_plaintext)
826833
doc.root[0]
827834
end

lib/onelogin/ruby-saml/utils.rb

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,13 @@ def self.decrypt_data(encrypted_node, private_key)
111111
symmetric_key = retrieve_symmetric_key(encrypt_data, private_key)
112112
cipher_value = REXML::XPath.first(
113113
encrypt_data,
114-
"//xenc:EncryptedData/xenc:CipherData/xenc:CipherValue",
114+
"./xenc:CipherData/xenc:CipherValue",
115115
{ 'xenc' => XENC }
116116
)
117117
node = Base64.decode64(cipher_value.text)
118118
encrypt_method = REXML::XPath.first(
119119
encrypt_data,
120-
"//xenc:EncryptedData/xenc:EncryptionMethod",
120+
"./xenc:EncryptionMethod",
121121
{ 'xenc' => XENC }
122122
)
123123
algorithm = encrypt_method.attributes['Algorithm']
@@ -131,10 +131,13 @@ def self.decrypt_data(encrypted_node, private_key)
131131
def self.retrieve_symmetric_key(encrypt_data, private_key)
132132
encrypted_key = REXML::XPath.first(
133133
encrypt_data,
134-
"//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey or \
135-
//xenc:EncryptedKey[@Id=substring-after(//xenc:EncryptedData/ds:KeyInfo/ds:RetrievalMethod/@URI, '#')]",
136-
{ "ds" => DSIG, "xenc" => XENC }
134+
"./ds:KeyInfo/xenc:EncryptedKey or \
135+
//xenc:EncryptedKey[@Id=$id]",
136+
{ "ds" => DSIG, "xenc" => XENC,
137+
"id" => self.retrieve_symetric_key_reference(encrypt_data)
138+
}
137139
)
140+
138141
encrypted_symmetric_key_element = REXML::XPath.first(
139142
encrypted_key,
140143
"./xenc:CipherData/xenc:CipherValue",
@@ -150,6 +153,14 @@ def self.retrieve_symmetric_key(encrypt_data, private_key)
150153
retrieve_plaintext(cipher_text, private_key, algorithm)
151154
end
152155

156+
def self.retrieve_symetric_key_reference(encrypt_data)
157+
REXML::XPath.first(
158+
encrypt_data,
159+
"substring-after(./ds:KeyInfo/ds:RetrievalMethod/@URI, '#')",
160+
{ "ds" => DSIG }
161+
)
162+
end
163+
153164
# Obtains the deciphered text
154165
# @param cipher_text [String] The ciphered text
155166
# @param symmetric_key [String] The symetric key used to encrypt the text

test/response_test.rb

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class RubySamlTest < Minitest::Test
2525
let(:response_no_statuscode) { OneLogin::RubySaml::Response.new(read_invalid_response("no_status_code.xml.base64")) }
2626
let(:response_statuscode_responder) { OneLogin::RubySaml::Response.new(read_invalid_response("status_code_responder.xml.base64")) }
2727
let(:response_statuscode_responder_and_msg) { OneLogin::RubySaml::Response.new(read_invalid_response("status_code_responer_and_msg.xml.base64")) }
28-
let(:response_encrypted_attrs) { OneLogin::RubySaml::Response.new(read_invalid_response("response_encrypted_attrs.xml.base64")) }
28+
let(:response_encrypted_attrs) { OneLogin::RubySaml::Response.new(response_document_encrypted_attrs) }
2929
let(:response_no_signed_elements) { OneLogin::RubySaml::Response.new(read_invalid_response("no_signature.xml.base64")) }
3030
let(:response_multiple_signed) { OneLogin::RubySaml::Response.new(read_invalid_response("multiple_signed.xml.base64")) }
3131
let(:response_invalid_audience) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_audience.xml.base64")) }
@@ -189,17 +189,6 @@ class RubySamlTest < Minitest::Test
189189
assert_includes response_valid_signed.errors, error_msg
190190
end
191191

192-
it "raise when the assertion contains encrypted attributes" do
193-
settings.idp_cert_fingerprint = signature_fingerprint_1
194-
response_encrypted_attrs.settings = settings
195-
response_encrypted_attrs.soft = false
196-
error_msg = "There is an EncryptedAttribute in the Response and this SP not support them"
197-
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
198-
response_encrypted_attrs.is_valid?
199-
end
200-
assert_includes response_encrypted_attrs.errors, error_msg
201-
end
202-
203192
it "raise when there is no valid audience" do
204193
settings.idp_cert_fingerprint = signature_fingerprint_1
205194
settings.issuer = 'invalid'
@@ -356,14 +345,6 @@ class RubySamlTest < Minitest::Test
356345
assert_includes response_valid_signed.errors, "The InResponseTo of the Response: _fc4a34b0-7efb-012e-caae-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid_request_id"
357346
end
358347

359-
it "return false when the assertion contains encrypted attributes" do
360-
settings.idp_cert_fingerprint = signature_fingerprint_1
361-
response_encrypted_attrs.settings = settings
362-
response_encrypted_attrs.soft = true
363-
response_encrypted_attrs.is_valid?
364-
assert_includes response_encrypted_attrs.errors, "There is an EncryptedAttribute in the Response and this SP not support them"
365-
end
366-
367348
it "return false when there is no valid audience" do
368349
settings.idp_cert_fingerprint = signature_fingerprint_1
369350
settings.issuer = 'invalid'
@@ -544,20 +525,6 @@ class RubySamlTest < Minitest::Test
544525
end
545526
end
546527

547-
describe "#validate_no_encrypted_attributes" do
548-
it "return true when the assertion does not contain encrypted attributes" do
549-
response_valid_signed.settings = settings
550-
assert response_valid_signed.send(:validate_no_encrypted_attributes)
551-
assert_empty response_valid_signed.errors
552-
end
553-
554-
it "return false when the assertion contains encrypted attributes" do
555-
response_encrypted_attrs.settings = settings
556-
assert !response_encrypted_attrs.send(:validate_no_encrypted_attributes)
557-
assert_includes response_encrypted_attrs.errors, "There is an EncryptedAttribute in the Response and this SP not support them"
558-
end
559-
end
560-
561528
describe "#validate_audience" do
562529
it "return true when the audience is valid" do
563530
response_valid_signed.settings = settings
@@ -858,15 +825,29 @@ class RubySamlTest < Minitest::Test
858825
assert_equal "bob", response_with_multiple_attribute_statements.attributes[:firstname]
859826
end
860827

861-
it "not raise errors about nil/empty attributes for EncryptedAttributes" do
862-
response_no_cert_and_encrypted_attrs = OneLogin::RubySaml::Response.new(response_document_no_cert_and_encrypted_attrs)
863-
assert_equal 'Demo', response_no_cert_and_encrypted_attrs.attributes["first_name"]
864-
end
865-
866828
it "not raise on responses without attributes" do
867829
assert_equal OneLogin::RubySaml::Attributes.new, response_unsigned.attributes
868830
end
869831

832+
describe "#encrypted attributes" do
833+
it "raise error when the assertion contains encrypted attributes but no private key to decrypt" do
834+
settings.private_key = nil
835+
response_encrypted_attrs.settings = settings
836+
#assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedAttribute found and no SP private key found on the settings to decrypt it") do
837+
# attrs = response_encrypted_attrs.attributes
838+
#end
839+
end
840+
841+
it "extract attributes when the assertion contains encrypted attributes and the private key is provided" do
842+
settings.certificate = ruby_saml_cert_text
843+
settings.private_key = ruby_saml_key_text
844+
response_encrypted_attrs.settings = settings
845+
attributes = response_encrypted_attrs.attributes
846+
assert_equal "test", attributes[:uid]
847+
assert_equal "test@example.com", attributes[:mail]
848+
end
849+
end
850+
870851
describe "#multiple values" do
871852
it "extract single value as string" do
872853
assert_equal "demo", response_multiple_attr_values.attributes[:uid]

test/responses/invalids/response_encrypted_attrs.xml.base64

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/test_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ def unsigned_message_encrypted_unsigned_assertion
131131
@unsigned_message_encrypted_unsigned_assertion ||= File.read(File.join(File.dirname(__FILE__), 'responses', 'unsigned_message_encrypted_unsigned_assertion.xml.base64'))
132132
end
133133

134+
def response_document_encrypted_attrs
135+
@response_document_encrypted_attrs ||= File.read(File.join(File.dirname(__FILE__), 'responses', 'response_encrypted_attrs.xml.base64'))
136+
end
137+
134138
def signature_fingerprint_1
135139
@signature_fingerprint1 ||= "C5:19:85:D9:47:F1:BE:57:08:20:25:05:08:46:EB:27:F6:CA:B7:83"
136140
end

0 commit comments

Comments
 (0)