Commit 24032d2

mo <mo@mokhan.ca>
2017-11-23 00:43:33
start working on crypto support.
1 parent 4f12933
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