Commit 8030342

mo <mo@mokhan.ca>
2018-02-13 00:14:38
provide a way to see if an individual signature is invalid.
1 parent 99fbb0a
lib/saml/kit/document.rb
@@ -60,7 +60,7 @@ module Saml
       #
       # @param pretty [Boolean] formats the xml or returns the raw xml.
       def to_xml(pretty: false)
-        pretty ? Nokogiri::XML(content).to_xml(indent: 2) : content
+        pretty ? to_nokogiri.to_xml(indent: 2) : content
       end
 
       # Returns the SAML document as an XHTML string. 
@@ -69,6 +69,10 @@ module Saml
         Nokogiri::XML(content, &:noblanks).to_xhtml
       end
 
+      def to_nokogiri
+        @nokogiri ||= Nokogiri::XML(content)
+      end
+
       def to_s
         to_xml
       end
lib/saml/kit/signature.rb
@@ -3,15 +3,21 @@ module Saml
     class Signature
       include ActiveModel::Validations
 
+      validate :validate_signature
+      #validate :validate_certificate
+
       def initialize(xml_hash)
-        @xml_hash = xml_hash
+        if xml_hash.is_a?(Hash)
+          @xml_hash = xml_hash
+        else
+          @document = xml_hash
+        end
       end
 
       # Returns the embedded X509 Certificate
       def certificate
-        value = to_h.fetch('KeyInfo', {}).fetch('X509Data', {}).fetch('X509Certificate', nil)
-        return if value.nil?
-        ::Xml::Kit::Certificate.new(value, use: :signing)
+        item = @document.at_xpath("//ds:KeyInfo/ds:X509Data/ds:X509Certificate", "ds": ::Xml::Kit::Namespaces::XMLDSIG)
+        ::Xml::Kit::Certificate.new(item.text, use: :signing)
       end
 
       # Returns true when the fingerprint of the certificate matches one of the certificates registered in the metadata.
@@ -24,6 +30,22 @@ module Saml
       def to_h
         @xml_hash
       end
+
+      private
+
+      def validate_signature
+        signature = Xmldsig::Signature.new(@document, 'ID=$uri or @Id')
+        unless signature.valid?(certificate.x509)
+          signature.errors.each { |error| errors.add(error, "is invalid") }
+        end
+      end
+
+      def validate_certificate(now = Time.current)
+        if certificate.expired?(now)
+          error_message = "Not valid before #{certificate.not_before}. Not valid after #{certificate.not_after}."
+          errors.add(:certificate, error_message)
+        end
+      end
     end
   end
 end
lib/saml/kit/trustable.rb
@@ -17,9 +17,10 @@ module Saml
       # @!visibility private
       def signature
         xml_hash = to_h.fetch(name, {}).fetch('Signature', nil)
-        xml_hash ? Signature.new(xml_hash) : nil
+        xml_hash ? Signature.new(to_nokogiri.at_xpath('//ds:Signature', 'ds' => Xml::Kit::Namespaces::XMLDSIG)) : nil
       end
 
+
       # Returns true when documents is signed and the signing certificate belongs to a known service entity.
       def trusted?
         return true if signature_manually_verified
spec/saml/authentication_request_spec.rb
@@ -228,4 +228,16 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
       expect(response).to be_instance_of(Saml::Kit::Response)
     end
   end
+
+  describe "#signature.valid?" do
+    it 'returns true when the signature is valid' do
+      expect(subject.signature).to be_valid
+    end
+
+    it 'returns false when the signature does not match' do
+      raw_xml.gsub!(issuer, 'corrupt')
+      subject = described_class.new(raw_xml, configuration: configuration)
+      expect(subject.signature).to_not be_valid
+    end
+  end
 end