Skip to content

Commit dab0227

Browse files
authored
Merge pull request SAML-Toolkits#357 from onelogin/encryptedattribute-support
Encryptedattribute support
2 parents 3a9d1fe + 9b8e8e2 commit dab0227

File tree

6 files changed

+78
-75
lines changed

6 files changed

+78
-75
lines changed

lib/onelogin/ruby-saml/response.rb

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def sessionindex
132132
# attributes['name']
133133
#
134134
# @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
135+
# @raise [ValidationError] if there are 2+ Attribute with the same Name
135136
#
136137
def attributes
137138
@attr_statements ||= begin
@@ -140,8 +141,19 @@ def attributes
140141
stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement')
141142
stmt_elements.each do |stmt_element|
142143
stmt_element.elements.each do |attr_element|
143-
name = attr_element.attributes["Name"]
144-
values = attr_element.elements.collect{|e|
144+
if attr_element.name == "EncryptedAttribute"
145+
node = decrypt_attribute(attr_element.dup)
146+
else
147+
node = attr_element
148+
end
149+
150+
name = node.attributes["Name"]
151+
152+
if options[:check_duplicated_attributes] && attributes.include?(name)
153+
raise ValidationError.new("Found an Attribute element with duplicated Name")
154+
end
155+
156+
values = node.elements.collect{|e|
145157
if (e.elements.nil? || e.elements.size == 0)
146158
# SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
147159
# otherwise the value is to be regarded as empty.
@@ -300,7 +312,6 @@ def validate(collect_errors = false)
300312
:validate_id,
301313
:validate_success_status,
302314
:validate_num_assertion,
303-
:validate_no_encrypted_attributes,
304315
:validate_no_duplicated_attributes,
305316
:validate_signed_elements,
306317
:validate_structure,
@@ -432,37 +443,17 @@ def validate_num_assertion
432443
true
433444
end
434445

435-
# Validates that there are not EncryptedAttribute (not supported)
436-
# If fails, the error is added to the errors array
437-
# @return [Boolean] True if there are no EncryptedAttribute elements, otherwise False if soft=True
438-
# @raise [ValidationError] if soft == false and validation fails
439-
#
440-
def validate_no_encrypted_attributes
441-
nodes = xpath_from_signed_assertion("/a:AttributeStatement/a:EncryptedAttribute")
442-
if nodes && nodes.length > 0
443-
return append_error("There is an EncryptedAttribute in the Response and this SP not support them")
444-
end
445-
446-
true
447-
end
448-
449446
# Validates that there are not duplicated attributes
450447
# If fails, the error is added to the errors array
451448
# @return [Boolean] True if there are no duplicated attribute elements, otherwise False if soft=True
452449
# @raise [ValidationError] if soft == false and validation fails
453450
#
454451
def validate_no_duplicated_attributes
455452
if options[:check_duplicated_attributes]
456-
processed_names = []
457-
stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement')
458-
stmt_elements.each do |stmt_element|
459-
stmt_element.elements.each do |attr_element|
460-
name = attr_element.attributes["Name"]
461-
if attributes.include?(name)
462-
return append_error("Found an Attribute element with duplicated Name")
463-
end
464-
processed_names.add(name)
465-
end
453+
begin
454+
attributes
455+
rescue ValidationError => e
456+
return append_error(e.message)
466457
end
467458
end
468459

@@ -928,22 +919,39 @@ def decrypt_nameid(encryptedid_node)
928919
decrypt_element(encryptedid_node, /(.*<\/(\w+:)?NameID>)/m)
929920
end
930921

922+
# Decrypts an EncryptedID element
923+
# @param encryptedid_node [REXML::Element] The EncryptedID element
924+
# @return [REXML::Document] The decrypted EncrypedtID element
925+
#
926+
def decrypt_attribute(encryptedattribute_node)
927+
decrypt_element(encryptedattribute_node, /(.*<\/(\w+:)?Attribute>)/m)
928+
end
929+
931930
# Decrypt an element
932931
# @param encryptedid_node [REXML::Element] The encrypted element
932+
# @param rgrex string Regex
933933
# @return [REXML::Document] The decrypted element
934934
#
935935
def decrypt_element(encrypt_node, rgrex)
936936
if settings.nil? || !settings.get_sp_key
937937
raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it')
938938
end
939939

940+
941+
if encrypt_node.name == 'EncryptedAttribute'
942+
node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
943+
else
944+
node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'
945+
end
946+
940947
elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key)
941948
# If we get some problematic noise in the plaintext after decrypting.
942949
# This quick regexp parse will grab only the Element and discard the noise.
943950
elem_plaintext = elem_plaintext.match(rgrex)[0]
944-
# To avoid namespace errors if saml namespace is not defined at assertion_plaintext
945-
# create a parent node first with the saml namespace defined
946-
elem_plaintext = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + elem_plaintext + '</node>'
951+
952+
# To avoid namespace errors if saml namespace is not defined
953+
# create a parent node first with the namespace defined
954+
elem_plaintext = node_header + elem_plaintext + '</node>'
947955
doc = REXML::Document.new(elem_plaintext)
948956
doc.root[0]
949957
end

lib/onelogin/ruby-saml/utils.rb

Lines changed: 15 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,12 @@ 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) }
137138
)
139+
138140
encrypted_symmetric_key_element = REXML::XPath.first(
139141
encrypted_key,
140142
"./xenc:CipherData/xenc:CipherValue",
@@ -150,6 +152,14 @@ def self.retrieve_symmetric_key(encrypt_data, private_key)
150152
retrieve_plaintext(cipher_text, private_key, algorithm)
151153
end
152154

155+
def self.retrieve_symetric_key_reference(encrypt_data)
156+
REXML::XPath.first(
157+
encrypt_data,
158+
"substring-after(./ds:KeyInfo/ds:RetrievalMethod/@URI, '#')",
159+
{ "ds" => DSIG }
160+
)
161+
end
162+
153163
# Obtains the deciphered text
154164
# @param cipher_text [String] The ciphered text
155165
# @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
@@ -28,7 +28,7 @@ class RubySamlTest < Minitest::Test
2828
let(:response_no_statuscode) { OneLogin::RubySaml::Response.new(read_invalid_response("no_status_code.xml.base64")) }
2929
let(:response_statuscode_responder) { OneLogin::RubySaml::Response.new(read_invalid_response("status_code_responder.xml.base64")) }
3030
let(:response_statuscode_responder_and_msg) { OneLogin::RubySaml::Response.new(read_invalid_response("status_code_responer_and_msg.xml.base64")) }
31-
let(:response_encrypted_attrs) { OneLogin::RubySaml::Response.new(read_invalid_response("response_encrypted_attrs.xml.base64")) }
31+
let(:response_encrypted_attrs) { OneLogin::RubySaml::Response.new(response_document_encrypted_attrs) }
3232
let(:response_no_signed_elements) { OneLogin::RubySaml::Response.new(read_invalid_response("no_signature.xml.base64")) }
3333
let(:response_multiple_signed) { OneLogin::RubySaml::Response.new(read_invalid_response("multiple_signed.xml.base64")) }
3434
let(:response_invalid_audience) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_audience.xml.base64")) }
@@ -198,17 +198,6 @@ class RubySamlTest < Minitest::Test
198198
assert_includes response_valid_signed.errors, error_msg
199199
end
200200

201-
it "raise when the assertion contains encrypted attributes" do
202-
settings.idp_cert_fingerprint = signature_fingerprint_1
203-
response_encrypted_attrs.settings = settings
204-
response_encrypted_attrs.soft = false
205-
error_msg = "There is an EncryptedAttribute in the Response and this SP not support them"
206-
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
207-
response_encrypted_attrs.is_valid?
208-
end
209-
assert_includes response_encrypted_attrs.errors, error_msg
210-
end
211-
212201
it "raise when there is no valid audience" do
213202
settings.idp_cert_fingerprint = signature_fingerprint_1
214203
settings.issuer = 'invalid'
@@ -365,14 +354,6 @@ class RubySamlTest < Minitest::Test
365354
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"
366355
end
367356

368-
it "return false when the assertion contains encrypted attributes" do
369-
settings.idp_cert_fingerprint = signature_fingerprint_1
370-
response_encrypted_attrs.settings = settings
371-
response_encrypted_attrs.soft = true
372-
response_encrypted_attrs.is_valid?
373-
assert_includes response_encrypted_attrs.errors, "There is an EncryptedAttribute in the Response and this SP not support them"
374-
end
375-
376357
it "return false when there is no valid audience" do
377358
settings.idp_cert_fingerprint = signature_fingerprint_1
378359
settings.issuer = 'invalid'
@@ -559,20 +540,6 @@ class RubySamlTest < Minitest::Test
559540
end
560541
end
561542

562-
describe "#validate_no_encrypted_attributes" do
563-
it "return true when the assertion does not contain encrypted attributes" do
564-
response_valid_signed.settings = settings
565-
assert response_valid_signed.send(:validate_no_encrypted_attributes)
566-
assert_empty response_valid_signed.errors
567-
end
568-
569-
it "return false when the assertion contains encrypted attributes" do
570-
response_encrypted_attrs.settings = settings
571-
assert !response_encrypted_attrs.send(:validate_no_encrypted_attributes)
572-
assert_includes response_encrypted_attrs.errors, "There is an EncryptedAttribute in the Response and this SP not support them"
573-
end
574-
end
575-
576543
describe "#validate_audience" do
577544
it "return true when the audience is valid" do
578545
response_valid_signed.settings = settings
@@ -953,15 +920,29 @@ class RubySamlTest < Minitest::Test
953920
assert_equal "bob", response_with_multiple_attribute_statements.attributes[:firstname]
954921
end
955922

956-
it "not raise errors about nil/empty attributes for EncryptedAttributes" do
957-
response_no_cert_and_encrypted_attrs = OneLogin::RubySaml::Response.new(response_document_no_cert_and_encrypted_attrs)
958-
assert_equal 'Demo', response_no_cert_and_encrypted_attrs.attributes["first_name"]
959-
end
960-
961923
it "not raise on responses without attributes" do
962924
assert_equal OneLogin::RubySaml::Attributes.new, response_unsigned.attributes
963925
end
964926

927+
describe "#encrypted attributes" do
928+
it "raise error when the assertion contains encrypted attributes but no private key to decrypt" do
929+
settings.private_key = nil
930+
response_encrypted_attrs.settings = settings
931+
assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedAttribute found and no SP private key found on the settings to decrypt it") do
932+
attrs = response_encrypted_attrs.attributes
933+
end
934+
end
935+
936+
it "extract attributes when the assertion contains encrypted attributes and the private key is provided" do
937+
settings.certificate = ruby_saml_cert_text
938+
settings.private_key = ruby_saml_key_text
939+
response_encrypted_attrs.settings = settings
940+
attributes = response_encrypted_attrs.attributes
941+
assert_equal "test", attributes[:uid]
942+
assert_equal "[email protected]", attributes[:mail]
943+
end
944+
end
945+
965946
it "return false when validating a response with duplicate attributes" do
966947
response_duplicated_attributes.settings = settings
967948
response_duplicated_attributes.options[:check_duplicated_attributes] = true

test/responses/invalids/response_encrypted_attrs.xml.base64

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

0 commit comments

Comments
 (0)