Commit 24032d2
Changed files (9)
lib/saml/kit/configuration.rb
@@ -7,6 +7,7 @@ module Saml
attr_accessor :issuer
attr_accessor :signature_method, :digest_method
attr_accessor :signing_certificate_pem, :signing_private_key_pem, :signing_private_key_password
+ attr_accessor :encryption_certificate_pem, :encryption_private_key_pem, :encryption_private_key_password
attr_accessor :registry, :session_timeout
attr_accessor :logger
@@ -14,7 +15,9 @@ module Saml
@signature_method = :SHA256
@digest_method = :SHA256
@signing_private_key_password = SecureRandom.uuid
+ @encryption_private_key_password = SecureRandom.uuid
@signing_certificate_pem, @signing_private_key_pem = SelfSignedCertificate.new(@signing_private_key_password).create
+ @encryption_certificate_pem, @encryption_private_key_pem = SelfSignedCertificate.new(@encryption_private_key_password).create
@registry = DefaultRegistry.new
@session_timeout = 3.hours
@logger = Logger.new(STDOUT)
@@ -31,6 +34,10 @@ module Saml
def signing_private_key
OpenSSL::PKey::RSA.new(signing_private_key_pem, signing_private_key_password)
end
+
+ def encryption_private_key
+ OpenSSL::PKey::RSA.new(encryption_private_key_pem, encryption_private_key_password)
+ end
end
end
end
lib/saml/kit/cryptography.rb
@@ -0,0 +1,68 @@
+module Saml
+ module Kit
+ class Cryptography
+ attr_reader :private_key
+
+ def initialize(private_key = Saml::Kit.configuration.encryption_private_key)
+ @private_key = private_key
+ end
+
+ #{
+ #"EncryptedData"=> {
+ #"xmlns:xenc"=>"http://www.w3.org/2001/04/xmlenc#",
+ #"xmlns:dsig"=>"http://www.w3.org/2000/09/xmldsig#",
+ #"Type"=>"http://www.w3.org/2001/04/xmlenc#Element",
+ #"EncryptionMethod"=> { "Algorithm"=>"http://www.w3.org/2001/04/xmlenc#aes128-cbc" },
+ #"KeyInfo"=> {
+ #"xmlns:dsig"=>"http://www.w3.org/2000/09/xmldsig#",
+ #"EncryptedKey"=>
+ #{
+ #"EncryptionMethod"=>{ "Algorithm"=>"http://www.w3.org/2001/04/xmlenc#rsa-1_5" },
+ #"CipherData"=>{ "CipherValue"=>"" }
+ #}
+ #},
+ #"CipherData"=>{ "CipherValue"=>"" }
+ #}
+ #}
+ def decrypt(data)
+ encrypt_data = data['EncryptedData']
+ symmetric_key = retrieve_symmetric_key(encrypt_data, private_key)
+ node = Base64.decode64(encrypt_data["CipherData"]["CipherValue"])
+ retrieve_plaintext(node, symmetric_key, encrypt_data["EncryptionMethod"]['Algorithm'])
+ end
+
+ private
+
+ def retrieve_symmetric_key(encrypted_data, private_key)
+ encrypted_key = encrypted_data['KeyInfo']['EncryptedKey']
+ cipher_text = Base64.decode64(encrypted_key['CipherData']['CipherValue'])
+ retrieve_plaintext(cipher_text, private_key, encrypted_key["EncryptionMethod"]['Algorithm'])
+ end
+
+ def retrieve_plaintext(cipher_text, symmetric_key, algorithm)
+ case algorithm
+ when 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' then cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').decrypt
+ when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt
+ when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt
+ when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
+ when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key
+ when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key
+ end
+
+ if cipher
+ iv_len = cipher.iv_len
+ data = cipher_text[iv_len..-1]
+ cipher.padding, cipher.key, cipher.iv = 0, symmetric_key, cipher_text[0..iv_len-1]
+ assertion_plaintext = cipher.update(data)
+ assertion_plaintext << cipher.final
+ elsif rsa
+ rsa.private_decrypt(cipher_text)
+ elsif oaep
+ oaep.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
+ else
+ cipher_text
+ end
+ end
+ end
+ end
+end
lib/saml/kit/identity_provider_metadata.rb
@@ -7,7 +7,7 @@ module Saml
def want_authn_requests_signed
xpath = "/md:EntityDescriptor/md:#{name}"
- attribute = find_by(xpath).attribute("WantAuthnRequestsSigned")
+ attribute = document.find_by(xpath).attribute("WantAuthnRequestsSigned")
return true if attribute.nil?
attribute.text.downcase == "true"
end
@@ -21,7 +21,7 @@ module Saml
end
def attributes
- find_all("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
+ document.find_all("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
{
format: item.attribute("NameFormat").try(:value),
name: item.attribute("Name").value,
lib/saml/kit/metadata.rb
@@ -3,14 +3,7 @@ module Saml
class Metadata
include ActiveModel::Validations
include XsdValidatable
-
METADATA_XSD = File.expand_path("./xsd/saml-schema-metadata-2.0.xsd", File.dirname(__FILE__)).freeze
- NAMESPACES = {
- "NameFormat": Namespaces::ATTR_SPLAT,
- "ds": Namespaces::XMLDSIG,
- "md": Namespaces::METADATA,
- "saml": Namespaces::ASSERTION,
- }.freeze
validates_presence_of :metadata
validate :must_contain_descriptor
@@ -27,16 +20,16 @@ module Saml
end
def entity_id
- find_by("/md:EntityDescriptor/@entityID").value
+ document.find_by("/md:EntityDescriptor/@entityID").value
end
def name_id_formats
- find_all("/md:EntityDescriptor/md:#{name}/md:NameIDFormat").map(&:text)
+ document.find_all("/md:EntityDescriptor/md:#{name}/md:NameIDFormat").map(&:text)
end
def certificates
- @certificates ||= find_all("/md:EntityDescriptor/md:#{name}/md:KeyDescriptor").map do |item|
- cert = item.at_xpath("./ds:KeyInfo/ds:X509Data/ds:X509Certificate", NAMESPACES).text
+ @certificates ||= document.find_all("/md:EntityDescriptor/md:#{name}/md:KeyDescriptor").map do |item|
+ cert = item.at_xpath("./ds:KeyInfo/ds:X509Data/ds:X509Certificate", Xml::NAMESPACES).text
{
text: cert,
fingerprint: Fingerprint.new(cert).algorithm(hash_algorithm),
@@ -54,7 +47,7 @@ module Saml
end
def services(type)
- find_all("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
+ document.find_all("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
binding = item.attribute("Binding").value
location = item.attribute("Location").value
binding_for(binding, location)
@@ -89,7 +82,7 @@ module Saml
end
def to_xml(pretty: false)
- pretty ? Nokogiri::XML(@xml).to_xml(indent: 2) : @xml
+ document.to_xml(pretty: pretty)
end
def to_s
@@ -117,19 +110,11 @@ module Saml
private
def document
- @document ||= Nokogiri::XML(@xml)
- end
-
- def find_by(xpath)
- document.at_xpath(xpath, NAMESPACES)
- end
-
- def find_all(xpath)
- document.search(xpath, NAMESPACES)
+ @document ||= Xml.new(xml)
end
def metadata
- find_by("/md:EntityDescriptor/md:#{name}").present?
+ document.find_by("/md:EntityDescriptor/md:#{name}").present?
end
def must_contain_descriptor
lib/saml/kit/response.rb
@@ -12,7 +12,7 @@ module Saml
end
def name_id
- to_h.fetch(name, {}).fetch('Assertion', {}).fetch('Subject', {}).fetch('NameID', nil)
+ assertion.fetch('Subject', {}).fetch('NameID', nil)
end
def [](key)
@@ -20,18 +20,17 @@ module Saml
end
def attributes
- @attributes ||= Hash[to_h.fetch(name, {}).fetch('Assertion', {}).fetch('AttributeStatement', {}).fetch('Attribute', []).map do |item|
+ @attributes ||= Hash[assertion.fetch('AttributeStatement', {}).fetch('Attribute', []).map do |item|
[item['Name'].to_sym, item['AttributeValue']]
end].with_indifferent_access
end
-
def started_at
- parse_date(to_h.fetch(name, {}).fetch('Assertion', {}).fetch('Conditions', {}).fetch('NotBefore', nil))
+ parse_date(assertion.fetch('Conditions', {}).fetch('NotBefore', nil))
end
def expired_at
- parse_date(to_h.fetch(name, {}).fetch('Assertion', {}).fetch('Conditions', {}).fetch('NotOnOrAfter', nil))
+ parse_date(assertion.fetch('Conditions', {}).fetch('NotOnOrAfter', nil))
end
def expired?
@@ -42,8 +41,21 @@ module Saml
Time.current > started_at && !expired?
end
+ def encrypted?
+ to_h[name]['EncryptedAssertion'].present?
+ end
+
+ def assertion
+ if encrypted?
+ decrypted = Cryptography.new.decrypt(to_h.fetch(name, {}).fetch('EncryptedAssertion', {}))
+ Hash.from_xml(decrypted)
+ else
+ to_h.fetch(name, {}).fetch('Assertion', {})
+ end
+ end
+
def signed?
- super || to_h.fetch(name, {}).fetch('Assertion', {}).fetch('Signature', nil).present?
+ super || assertion.fetch('Signature', nil).present?
end
def certificate
lib/saml/kit/service_provider_metadata.rb
@@ -14,7 +14,7 @@ module Saml
end
def want_assertions_signed
- attribute = find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
+ attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
attribute.text.downcase == "true"
end
lib/saml/kit/xml.rb
@@ -2,6 +2,12 @@ module Saml
module Kit
class Xml
include ActiveModel::Validations
+ NAMESPACES = {
+ "NameFormat": Namespaces::ATTR_SPLAT,
+ "ds": Namespaces::XMLDSIG,
+ "md": Namespaces::METADATA,
+ "saml": Namespaces::ASSERTION,
+ }.freeze
attr_reader :raw_xml, :document
@@ -10,9 +16,7 @@ module Saml
def initialize(raw_xml)
@raw_xml = raw_xml
- @document = Nokogiri::XML(raw_xml, nil, nil, Nokogiri::XML::ParseOptions::STRICT) do |config|
- config.noblanks
- end
+ @document = Nokogiri::XML(raw_xml)
end
def x509_certificates
@@ -22,6 +26,18 @@ module Saml
end
end
+ def find_by(xpath)
+ document.at_xpath(xpath, NAMESPACES)
+ end
+
+ def find_all(xpath)
+ document.search(xpath, NAMESPACES)
+ end
+
+ def to_xml(pretty: true)
+ pretty ? document.to_xml(indent: 2) : raw_xml
+ end
+
private
def validate_signatures
lib/saml/kit.rb
@@ -23,6 +23,7 @@ require "saml/kit/document"
require "saml/kit/authentication_request"
require "saml/kit/bindings"
require "saml/kit/configuration"
+require "saml/kit/cryptography"
require "saml/kit/default_registry"
require "saml/kit/fingerprint"
require "saml/kit/logout_response"
spec/saml/response_spec.rb
@@ -422,4 +422,40 @@ RSpec.describe Saml::Kit::Response do
end
end
end
+
+ describe "encrypted assertion" do
+ it 'parses the encrypted assertion' do
+ id = SecureRandom.uuid
+ now = Time.now.utc
+ acs_url = FFaker::Internet.uri("https")
+ xml = <<-XML
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" xmlns:saml="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{acs_url}" InResponseTo="_#{SecureRandom.uuid}">
+ <saml:Issuer>#{FFaker::Internet.uri("https")}</saml:Issuer>
+ <samlp:Status>
+ <samlp:StatusCode Value="#{Saml::Kit::Namespaces::SUCCESS}"/>
+ </samlp:Status>
+ <saml:EncryptedAssertion>
+ <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Type="http://www.w3.org/2001/04/xmlenc#Element">
+ <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+ <dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
+ <xenc:EncryptedKey>
+ <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
+ <xenc:CipherData>
+ <xenc:CipherValue>KRlkBAccafKExzq07FsT/rLRH37UM6kPGlgxrUOP+sOggqqgzUn0uSR0m2d4ZLAEoHCc6VefZKHv8s9xchWliu4Lgxff+9Sfybqjd/MQmvL7zkZ4MELcGZcm73SHUFbK3yZzx6imczabR+K5+tIn7q9jYyQqw05DdD39LmbVvDI=</xenc:CipherValue>
+ </xenc:CipherData>
+ </xenc:EncryptedKey>
+ </dsig:KeyInfo>
+ <xenc:CipherData>
+ <xenc:CipherValue>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</xenc:CipherValue>
+ </xenc:CipherData>
+</xenc:EncryptedData>
+ </saml:EncryptedAssertion>
+</samlp:Response>
+XML
+
+ subject = described_class.new(xml)
+ expect(subject).to be_encrypted
+ expect(subject.attributes).to be_present
+ end
+ end
end