lib/saml/util.rb
module Saml
class Util
class << self
def parse_params(url)
query = URI.parse(url).query
return {} unless query
params = {}
query.split(/[&;]/).each do |pairs|
key, value = pairs.split('=', 2)
params[key] = value
end
params
end
def post(location, message, additional_headers = {}, proxy = {})
uri = URI.parse(location)
default_proxy_settings = { addr: :ENV, port: nil, user: nil, pass: nil }
proxy = default_proxy_settings.merge(proxy)
http = Net::HTTP.new(uri.host, uri.port, proxy[:addr], proxy[:port], proxy[:user], proxy[:pass])
http.use_ssl = uri.scheme == 'https'
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
add_cacert_file(http)
add_ssl_certificate_and_key(http)
request = Net::HTTP::Post.new(uri.request_uri, merged_headers(additional_headers))
request.body = message
http.request(request)
end
def sign_xml(message, format = :xml, include_nested_prefixlist = false, &block)
message.add_signature
document = Xmldsig::SignedDocument.new(message.send("to_#{format}"))
if Saml::Config.include_nested_prefixlist || include_nested_prefixlist
document.signatures.reverse.each_with_object([]) do |signature, nested_prefixlist|
inclusive_namespaces = signature.signature.at_xpath('descendant::ec:InclusiveNamespaces', Xmldsig::NAMESPACES)
if inclusive_namespaces
nested_prefixlist.concat(inclusive_namespaces.get_attribute('PrefixList').to_s.split(' '))
if signature.unsigned?
inclusive_namespaces.set_attribute('PrefixList', nested_prefixlist.uniq.join(' '))
end
end
end
end
if block_given?
document.sign(&block)
else
document.sign do |data, signature_algorithm|
message.provider.sign(signature_algorithm, data)
end
end
end
def encrypt_assertion(assertion, key_descriptor_or_certificate, include_certificate: false, include_key_retrieval_method: false)
case key_descriptor_or_certificate
when OpenSSL::X509::Certificate
certificate = key_descriptor_or_certificate
key_name = nil
when Saml::Elements::KeyDescriptor
certificate = key_descriptor_or_certificate.certificate
key_name = key_descriptor_or_certificate.key_info.key_name
else
fail ArgumentError, "Expecting Certificate or KeyDescriptor got: #{key_descriptor_or_certificate.class}"
end
assertion = assertion.to_xml(nil, nil, false) if assertion.is_a?(Assertion) # create xml without instruct
encrypted_data = Xmlenc::Builder::EncryptedData.new
encrypted_data.set_encryption_method(algorithm: 'http://www.w3.org/2001/04/xmlenc#aes128-cbc')
encrypted_key = encrypted_data.encrypt(assertion.to_s)
encrypted_key.set_encryption_method(algorithm: 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p',
digest_method_algorithm: 'http://www.w3.org/2000/09/xmldsig#sha1')
encrypted_key.key_info = if include_certificate || key_name
key_info = Saml::Elements::KeyInfo.new(include_certificate ? certificate.to_pem : nil)
key_info.key_name = key_name
key_info
end
encrypted_key.encrypt(certificate.public_key)
if include_key_retrieval_method
encrypted_key.id = '_' + SecureRandom.uuid
encrypted_data.set_key_retrieval_method (Xmlenc::Builder::RetrievalMethod.new(uri: "##{encrypted_key.id}"))
end
Saml::Elements::EncryptedAssertion.new(encrypted_data: encrypted_data, encrypted_keys: encrypted_key)
end
def decrypt_assertion(encrypted_assertion, private_key)
encrypted_assertion_xml = encrypted_assertion.is_a?(Saml::Elements::EncryptedAssertion) ?
encrypted_assertion.to_xml : encrypted_assertion.to_s
encrypted_document = Xmlenc::EncryptedDocument.new(encrypted_assertion_xml)
Saml::Assertion.parse(encrypted_document.decrypt(private_key), single: true)
end
def encrypt_element(element, target_element, encrypted_key_data, encrypted_data_options)
key_name = encrypted_data_options.fetch(:key_name, Saml.generate_id)
element.encrypted_data = Xmlenc::Builder::EncryptedData.new(encrypted_data_options)
element.encrypted_data.set_encryption_method(algorithm: 'http://www.w3.org/2001/04/xmlenc#aes256-cbc')
element.encrypted_data.set_key_name key_name
original_encrypted_key = element.encrypted_data.encrypt(Nokogiri::XML(target_element.to_xml).root.to_xml, encrypted_data_options)
encrypted_key_data.each do |key_descriptor, key_options = {}|
encrypted_key_options = key_options.merge(id: Saml.generate_id, data: original_encrypted_key.data)
encrypted_key = Xmlenc::Builder::EncryptedKey.new(encrypted_key_options)
encrypted_key.add_data_reference(element.encrypted_data.id)
encrypted_key.set_key_name(key_descriptor.key_info.key_name)
encrypted_key.carried_key_name = key_name
encrypted_key.set_encryption_method(algorithm: 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p', digest_method_algorithm: 'http://www.w3.org/2000/09/xmldsig#sha1')
encrypted_key.encrypt(key_descriptor.certificate.public_key)
element.encrypted_keys ||= []
element.encrypted_keys << encrypted_key
end
element
end
def encrypt_name_id(name_id, key_descriptor, key_options = {})
encrypted_id = Saml::Elements::EncryptedID.new(name_id: name_id)
encrypt_encrypted_id(encrypted_id, key_descriptor, key_options)
end
def encrypt_encrypted_id(encrypted_id, key_descriptor, key_options = {})
encrypted_id.encrypt(key_descriptor, key_options)
encrypted_id
end
def decrypt_encrypted_id(encrypted_id, private_key, fail_silent = false)
encrypted_id_xml = encrypted_id.is_a?(Saml::Elements::EncryptedID) ?
encrypted_id.to_xml : encrypted_id.to_s
encrypted_document = Xmlenc::EncryptedDocument.new(encrypted_id_xml)
Saml::Elements::EncryptedID.parse(encrypted_document.decrypt(private_key, fail_silent))
end
def verify_xml(message, raw_body)
document = Xmldsig::SignedDocument.new(raw_body)
signature_valid = document.validate do |signature, data, signature_algorithm|
node = document.signatures.find { |s| s.signature_value == signature }.signature.at_xpath('descendant::ds:KeyName', Xmldsig::NAMESPACES)
key_name = node.present? ? node.content : nil
message.provider.verify(signature_algorithm, signature, data, key_name)
end
fail Saml::Errors::SignatureInvalid unless signature_valid
signed_node = document.signed_nodes.find { |node| node['ID'] == message._id }
fail Saml::Errors::SignatureMissing unless signed_node
message.class.parse(signed_node.canonicalize, single: true)
end
def collect_extra_namespaces(raw_xml)
doc = Nokogiri::XML(raw_xml, nil, nil, Nokogiri::XML::ParseOptions::STRICT)
doc.collect_namespaces.each_with_object({}) { |(prefix, path), hash| hash[prefix.gsub('xmlns:', '')] = path }
end
def download_metadata_xml(location)
uri = URI.parse(location)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
add_cacert_file(http)
request = Net::HTTP::Get.new(uri.request_uri)
response = http.request(request)
if response.code == '200'
response.body
else
fail Saml::Errors::MetadataDownloadFailed, "Cannot download metadata for: #{location}: #{response.body}"
end
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse,
Net::HTTPHeaderSyntaxError, Net::ProtocolError => error
raise Saml::Errors::MetadataDownloadFailed, "Cannot download metadata for: #{location}: #{error.message}"
end
private
def merged_headers(headers)
{ 'Content-Type' => 'text/xml',
'Cache-Control' => 'no-cache, no-store',
'Pragma' => 'no-cache' }.merge(headers)
end
def add_cacert_file(http)
return http unless Saml::Config.http_ca_file.present?
http.cert_store = OpenSSL::X509::Store.new
http.cert_store.set_default_paths
http.cert_store.add_file(Saml::Config.http_ca_file)
http
end
def add_ssl_certificate_and_key(http)
return http unless Saml::Config.ssl_certificate.present?
return http unless Saml::Config.ssl_private_key.present?
http.key = Saml::Config.ssl_private_key
http.cert = Saml::Config.ssl_certificate
http
end
end
end
end