-
-
Notifications
You must be signed in to change notification settings - Fork 569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add decrypt support. #241
Add decrypt support. #241
Changes from 5 commits
eaa3a58
629b421
bfd0d2d
50bd166
3867506
b164dd7
5b7b7c6
5a2a943
8d874ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ class Response < SamlMessage | |
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" | ||
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" | ||
DSIG = "http://www.w3.org/2000/09/xmldsig#" | ||
XENC = "http://www.w3.org/2001/04/xmlenc#" | ||
|
||
# TODO: Settings should probably be initialized too... WDYT? | ||
|
||
|
@@ -24,6 +25,7 @@ class Response < SamlMessage | |
attr_accessor :errors | ||
|
||
attr_reader :document | ||
attr_reader :decrypted_document | ||
attr_reader :response | ||
attr_reader :options | ||
|
||
|
@@ -39,7 +41,7 @@ def initialize(response, options = {}) | |
@errors = [] | ||
|
||
raise ArgumentError.new("Response cannot be nil") if response.nil? | ||
@options = options | ||
@options = options | ||
|
||
@soft = true | ||
if !options.empty? && !options[:settings].nil? | ||
|
@@ -51,6 +53,21 @@ def initialize(response, options = {}) | |
|
||
@response = decode_raw_saml(response) | ||
@document = XMLSecurity::SignedDocument.new(@response, @errors) | ||
|
||
if assertion_encrypted? | ||
if @settings.nil? || [email protected]_sp_key | ||
validation_error('An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method') | ||
end | ||
|
||
# Marshal at Ruby 1.8.7 throw an Exception | ||
if RUBY_VERSION < "1.9" | ||
@decrypted_document = XMLSecurity::SignedDocument.new(@response, @errors) | ||
else | ||
@decrypted_document = Marshal.load(Marshal.dump(@document)) | ||
end | ||
|
||
@decrypted_document = document_with_decrypted_assertion | ||
end | ||
end | ||
|
||
# Append the cause to the errors array, and based on the value of soft, return false or raise | ||
|
@@ -76,7 +93,12 @@ def is_valid? | |
# | ||
def name_id | ||
@name_id ||= begin | ||
node = xpath_first_from_signed_assertion('/a:Subject/a:NameID') | ||
enc_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would complete the name to make it more readable. encrypted_node |
||
if enc_node | ||
node = decrypt_nameid(enc_node) | ||
else | ||
node = xpath_first_from_signed_assertion('/a:Subject/a:NameID') | ||
end | ||
node.nil? ? nil : node.text | ||
end | ||
end | ||
|
@@ -205,9 +227,10 @@ def issuers | |
issuers = [] | ||
nodes = REXML::XPath.match( | ||
document, | ||
"/p:Response/a:Issuer | /p:Response/a:Assertion/a:Issuer", | ||
"/p:Response/a:Issuer", | ||
{ "p" => PROTOCOL, "a" => ASSERTION } | ||
) | ||
nodes += xpath_from_signed_assertion("/a:Issuer") | ||
nodes.each do |node| | ||
issuers << node.text if node.text | ||
end | ||
|
@@ -371,11 +394,7 @@ def validate_num_assertion | |
# @raise [ValidationError] if soft == false and validation fails | ||
# | ||
def validate_no_encrypted_attributes | ||
nodes = REXML::XPath.match( | ||
document, | ||
"/p:Response/a:Assertion/a:AttributeStatement/a:EncryptedAttribute", | ||
{ "p" => PROTOCOL, "a" => ASSERTION } | ||
) | ||
nodes = xpath_from_signed_assertion("/a:AttributeStatement/a:EncryptedAttribute") | ||
if nodes && nodes.length > 0 | ||
return append_error("There is an EncryptedAttribute in the Response and this SP not support them") | ||
end | ||
|
@@ -391,7 +410,7 @@ def validate_no_encrypted_attributes | |
# | ||
def validate_signed_elements | ||
signature_nodes = REXML::XPath.match( | ||
document, | ||
decrypted_document.nil? ? document : decrypted_document, | ||
"//ds:Signature", | ||
{"ds"=>DSIG} | ||
) | ||
|
@@ -452,13 +471,13 @@ def validate_conditions | |
|
||
now = Time.now.utc | ||
|
||
if not_before && (now + (options[:allowed_clock_drift] || 0)) < not_before | ||
error_msg = "Current time is earlier than NotBefore condition #{(now + (options[:allowed_clock_drift] || 0))} < #{not_before})" | ||
if not_before && (now + allowed_clock_drift) < not_before | ||
error_msg = "Current time is earlier than NotBefore condition #{(now + allowed_clock_drift)} < #{not_before})" | ||
return append_error(error_msg) | ||
end | ||
|
||
if not_on_or_after && now >= not_on_or_after | ||
error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after})" | ||
if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift) | ||
error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after + allowed_clock_drift})" | ||
return append_error(error_msg) | ||
end | ||
|
||
|
@@ -551,7 +570,17 @@ def validate_subject_confirmation | |
def validate_signature | ||
fingerprint = settings.get_fingerprint | ||
|
||
unless fingerprint && document.validate_document(fingerprint, soft, :fingerprint_alg => settings.idp_cert_fingerprint_algorithm) | ||
# If the response contains the signature, and the assertion was encrypted, validate the original SAML Response | ||
# otherwise, review if the decrypted assertion contains a signature | ||
response_signed = REXML::XPath.first( | ||
document, | ||
"/p:Response[@ID=$id]", | ||
{ "p" => PROTOCOL, "ds" => DSIG }, | ||
{ 'id' => document.signed_element_id } | ||
) | ||
doc = (response_signed || decrypted_document.nil?) ? document : decrypted_document | ||
|
||
unless fingerprint && doc.validate_document(fingerprint, :fingerprint_alg => settings.idp_cert_fingerprint_algorithm) | ||
error_msg = "Invalid Signature on SAML Response" | ||
return append_error(error_msg) | ||
end | ||
|
@@ -565,17 +594,18 @@ def validate_signature | |
# @return [REXML::Element | nil] If any matches, return the Element | ||
# | ||
def xpath_first_from_signed_assertion(subelt=nil) | ||
doc = decrypted_document.nil? ? document : decrypted_document | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line is repeated, maybe it worth a method? |
||
node = REXML::XPath.first( | ||
document, | ||
doc, | ||
"/p:Response/a:Assertion[@ID=$id]#{subelt}", | ||
{ "p" => PROTOCOL, "a" => ASSERTION }, | ||
{ 'id' => document.signed_element_id } | ||
{ 'id' => doc.signed_element_id } | ||
) | ||
node ||= REXML::XPath.first( | ||
document, | ||
doc, | ||
"/p:Response[@ID=$id]/a:Assertion#{subelt}", | ||
{ "p" => PROTOCOL, "a" => ASSERTION }, | ||
{ 'id' => document.signed_element_id } | ||
{ 'id' => doc.signed_element_id } | ||
) | ||
node | ||
end | ||
|
@@ -586,20 +616,92 @@ def xpath_first_from_signed_assertion(subelt=nil) | |
# @return [Array of REXML::Element] Return all matches | ||
# | ||
def xpath_from_signed_assertion(subelt=nil) | ||
doc = decrypted_document.nil? ? document : decrypted_document | ||
node = REXML::XPath.match( | ||
document, | ||
doc, | ||
"/p:Response/a:Assertion[@ID=$id]#{subelt}", | ||
{ "p" => PROTOCOL, "a" => ASSERTION }, | ||
{ 'id' => document.signed_element_id } | ||
{ 'id' => doc.signed_element_id } | ||
) | ||
node.concat( REXML::XPath.match( | ||
document, | ||
doc, | ||
"/p:Response[@ID=$id]/a:Assertion#{subelt}", | ||
{ "p" => PROTOCOL, "a" => ASSERTION }, | ||
{ 'id' => document.signed_element_id } | ||
{ 'id' => doc.signed_element_id } | ||
)) | ||
end | ||
|
||
# Obtains a SAML Response with the EncryptedAssertion element decrypted | ||
# @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted | ||
# | ||
def document_with_decrypted_assertion | ||
response_node = REXML::XPath.first( | ||
decrypted_document, | ||
"/p:Response/", | ||
{ "p" => PROTOCOL } | ||
) | ||
encrypted_assertion_node = REXML::XPath.first( | ||
decrypted_document, | ||
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", | ||
{ "p" => PROTOCOL, "a" => ASSERTION } | ||
) | ||
response_node.add(decrypt_assertion(encrypted_assertion_node)) | ||
encrypted_assertion_node.remove | ||
XMLSecurity::SignedDocument.new(response_node.to_s) | ||
end | ||
|
||
# Checks if the SAML Response contains or not an EncryptedAssertion element | ||
# @return [Boolean] True if the SAML Response contains an EncryptedAssertion element | ||
# | ||
def assertion_encrypted? | ||
encrypted_node = REXML::XPath.first( | ||
document, | ||
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", | ||
{ "p" => PROTOCOL, "a" => ASSERTION } | ||
) | ||
!encrypted_node.nil? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would make this simpler:
|
||
end | ||
|
||
# Decrypts an EncryptedAssertion element | ||
# @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element | ||
# @return [REXML::Document] The decrypted EncryptedAssertion element | ||
# | ||
def decrypt_assertion(encrypted_assertion_node) | ||
if settings.nil? || !settings.get_sp_key | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this can be expressed with an eager return:
|
||
validation_error('An EncryptedAssertion found and no SP private key found on the settings to decrypt it') | ||
else | ||
assertion_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypted_assertion_node, settings.get_sp_key) | ||
# If we get some problematic noise in the plaintext after decrypting. | ||
# This quick regexp parse will grab only the assertion and discard the noise. | ||
assertion_plaintext = assertion_plaintext.match(/(.*<\/(saml:|)Assertion>)/m)[0] | ||
# To avoid namespace errors if saml namespace is not defined at assertion_plaintext | ||
# create a parent node first with the saml namespace defined | ||
assertion_plaintext = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'+ assertion_plaintext + '</node>' | ||
doc = REXML::Document.new(assertion_plaintext) | ||
doc.root[0] | ||
end | ||
end | ||
|
||
# Decrypts an EncryptedID element | ||
# @param encryptedid_node [REXML::Element] The EncryptedID element | ||
# @return [REXML::Document] The decrypted EncrypedtID element | ||
# | ||
def decrypt_nameid(encryptedid_node) | ||
if settings.nil? || !settings.get_sp_key | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. eager return here too There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will refactor both methods ;) |
||
validation_error('An EncryptedID found and no SP private key found on the settings to decrypt it') | ||
else | ||
nameid_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encryptedid_node, settings.get_sp_key) | ||
# If we get some problematic noise in the plaintext after decrypting. | ||
# This quick regexp parse will grab only the NameID and discard the noise. | ||
nameid_plaintext = nameid_plaintext.match(/(.*<\/(saml:|)NameID>)/m)[0] | ||
# To avoid namespace errors if saml namespace is not defined at assertion_plaintext | ||
# create a parent node first with the saml namespace defined | ||
nameid_plaintext = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'+ nameid_plaintext + '</node>' | ||
doc = REXML::Document.new(nameid_plaintext) | ||
doc.root[0] | ||
end | ||
end | ||
|
||
# Parse the attribute of a given node in Time format | ||
# @param node [REXML:Element] The node | ||
# @param attribute [String] The attribute name | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that will save you from indenting the following lines