Commit dfdb79d

mo <mo.khan@gmail.com>
2017-10-28 22:09:49
validate signature of idp metadata.
1 parent 7a5e074
lib/saml/kit/locales/en.yml
@@ -6,3 +6,4 @@ en:
         metadata:
           invalid_idp: "must contain identity provider metadata."
           blank: "metadata is required."
+          invalid_signature: "invalid signature."
lib/saml/kit/identity_provider_metadata.rb
@@ -13,6 +13,7 @@ module Saml
       validates_presence_of :metadata
       validate :must_contain_idp_descriptor
       validate :must_match_xsd
+      validate :must_have_valid_signature
 
       def initialize(xml)
         @xml = xml
@@ -89,6 +90,15 @@ module Saml
         end
       end
 
+      def must_have_valid_signature
+        return if to_xml.blank?
+        errors[:metadata] << error_message('metadata.invalid_signature') unless valid_signature?
+      end
+
+      def valid_signature?
+        Saml::Kit::Xml.new(to_xml).valid?
+      end
+
       def fingerprint_for(value)
         x509 = OpenSSL::X509::Certificate.new(value)
         OpenSSL::Digest::SHA256.new.hexdigest(x509.to_der).upcase.scan(/../).join(":")
lib/saml/kit/xml.rb
@@ -0,0 +1,57 @@
+module Saml
+  module Kit
+    class Xml
+      include ActiveModel::Validations
+
+      attr_reader :raw_xml, :document
+
+      validate :validate_signature
+      validate :validate_certificate, if: :signature_element
+
+      def initialize(raw_xml)
+        @raw_xml = raw_xml
+        @document = Nokogiri::XML(raw_xml, nil, nil, Nokogiri::XML::ParseOptions::STRICT) do |config|
+          config.noblanks
+        end
+      end
+
+      def signature_element
+        document.at_xpath('//ds:Signature', Xmldsig::NAMESPACES)
+      end
+
+      def certificate
+        xpath = '//ds:KeyInfo/ds:X509Data/ds:X509Certificate'
+        raw_signature = signature_element.xpath(xpath, Xmldsig::NAMESPACES).text
+        OpenSSL::X509::Certificate.new(Base64.decode64(raw_signature))
+      end
+
+      private
+
+      def validate_signature
+        invalid_signatures.flat_map(&:errors).uniq.each do |error|
+          errors.add(error, "is invalid") if error != :signature
+        end
+      end
+
+      def signed_document
+        Xmldsig::SignedDocument.new(document, id_attr: 'ID=$uri or @Id')
+      end
+
+      def invalid_signatures
+        signed_document.signatures.find_all do |signature|
+          !signature.valid?(certificate)
+        end
+      end
+
+      def validate_certificate(now = Time.current)
+        if now < certificate.not_before
+          errors.add(:certificate, "Not valid before #{certificate.not_before}")
+        end
+
+        if now > certificate.not_after
+          errors.add(:certificate, "Not valid after #{certificate.not_after}")
+        end
+      end
+    end
+  end
+end
lib/saml/kit.rb
@@ -1,12 +1,14 @@
 require "saml/kit/version"
 
+require "active_model"
+require "active_support/core_ext/hash/conversions"
+require "active_support/core_ext/numeric/time"
+require "active_support/duration"
 require "builder"
 require "nokogiri"
 require "securerandom"
-require "active_model"
-require "active_support/duration"
-require "active_support/core_ext/numeric/time"
-require "active_support/core_ext/hash/conversions"
+require "xmldsig"
+
 require "saml/kit/authentication_request"
 require "saml/kit/configuration"
 require "saml/kit/namespaces"
@@ -15,6 +17,7 @@ require "saml/kit/response"
 require "saml/kit/service_provider_registry"
 require "saml/kit/identity_provider_metadata"
 require "saml/kit/service_provider_metadata"
+require "saml/kit/xml"
 
 I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
 
spec/saml/identity_provider_metadata_spec.rb
@@ -222,21 +222,15 @@ EOS
         metadata_xml = IO.read("spec/fixtures/metadata/ad_2012.xml").gsub(old_url, new_url)
 
         subject = described_class.new(metadata_xml)
-        subject.validate do |error|
-          errors << error
-        end
-        expect(errors).to be_present
-        expect(errors[0].message).to eql(I18n.translate("activerecord.errors.models.sso_configuration.attributes.metadata.invalid_signature"))
+        expect(subject).to be_invalid
+        expect(subject.errors[:metadata]).to include("invalid signature.")
       end
 
       it 'is valid, when the content has not been tampered with' do
         metadata_xml = IO.read("spec/fixtures/metadata/ad_2012.xml")
 
         subject = described_class.new(metadata_xml)
-        subject.validate do |error|
-          errors << error
-        end
-        expect(errors).to be_empty
+        expect(subject).to be_valid
       end
     end
   end
saml-kit.gemspec
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
   spec.add_dependency "activesupport", "~> 5.1"
   spec.add_dependency "builder", "~> 3.2"
   spec.add_dependency "nokogiri", "~> 1.8"
+  spec.add_dependency "xmldsig", "~> 0.6"
   spec.add_development_dependency "bundler", "~> 1.15"
   spec.add_development_dependency "rake", "~> 10.0"
   spec.add_development_dependency "rspec", "~> 3.0"