Commit c42c5fa

mo <mo@mokhan.ca>
2017-11-02 17:04:22
add specs for xml.
1 parent a3f93bd
Changed files (2)
lib
saml
spec
lib/saml/kit/xml.rb
@@ -6,7 +6,7 @@ module Saml
       attr_reader :raw_xml, :document
 
       validate :validate_signature
-      validate :validate_certificate, if: :signature_element
+      validate :validate_certificate
 
       def initialize(raw_xml)
         @raw_xml = raw_xml
@@ -15,41 +15,41 @@ module Saml
         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))
+      def x509_certificates
+        xpath = "//ds:KeyInfo/ds:X509Data/ds:X509Certificate"
+        document.search(xpath, Xmldsig::NAMESPACES).map do |item|
+          OpenSSL::X509::Certificate.new(Base64.decode64(item.text))
+        end
       end
 
       private
 
       def validate_signature
         invalid_signatures.flat_map(&:errors).uniq.each do |error|
-          errors.add(error, "is invalid") if error != :signature
+          errors.add(error, "is invalid")
         end
       end
 
-      def signed_document
-        Xmldsig::SignedDocument.new(document, id_attr: 'ID=$uri or @Id')
-      end
-
       def invalid_signatures
+        signed_document = Xmldsig::SignedDocument.new(document, id_attr: 'ID=$uri or @Id')
         signed_document.signatures.find_all do |signature|
-          !signature.valid?(certificate)
+          x509_certificates.all? do |certificate|
+            !signature.valid?(certificate)
+          end
         end
       end
 
       def validate_certificate(now = Time.current)
-        if now < certificate.not_before
-          errors.add(:certificate, "Not valid before #{certificate.not_before}")
-        end
+        return unless document.at_xpath('//ds:Signature', Xmldsig::NAMESPACES).present?
+
+        x509_certificates.each do |certificate|
+          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}")
+          if now > certificate.not_after
+            errors.add(:certificate, "Not valid after #{certificate.not_after}")
+          end
         end
       end
     end
spec/saml/xml_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Xml do
+  describe "#valid_signature?" do
+    let(:login_url) { "https://#{FFaker::Internet.domain_name}/login" }
+    let(:logout_url) { "https://#{FFaker::Internet.domain_name}/logout" }
+
+    let(:signed_xml) do
+      builder = Saml::Kit::ServiceProviderMetadata::Builder.new
+      builder.entity_id = FFaker::Movie.title
+      builder.add_assertion_consumer_service(login_url, binding: :post)
+      builder.add_assertion_consumer_service(login_url, binding: :http_redirect)
+      builder.add_single_logout_service(logout_url, binding: :post)
+      builder.add_single_logout_service(logout_url, binding: :http_redirect)
+      builder.to_xml
+    end
+
+    it 'returns true, when the digest and signature is valid' do
+      subject = described_class.new(signed_xml)
+      expect(subject).to be_valid
+    end
+
+    it 'returns false, when the SHA1 digest is not valid' do
+      subject = described_class.new(signed_xml.gsub("EntityDescriptor", "uhoh"))
+      expect(subject).to_not be_valid
+      expect(subject.errors[:digest_value]).to be_present
+    end
+
+    it 'it is invalid when digest is incorrect' do
+      old_digest = Hash.from_xml(signed_xml)['EntityDescriptor']['Signature']['SignedInfo']['Reference']['DigestValue']
+      subject = described_class.new(signed_xml.gsub(old_digest, 'sabotage'))
+      expect(subject).to_not be_valid
+      expect(subject.errors[:digest_value]).to be_present
+    end
+
+    it 'returns false, when the signature is invalid' do
+      old_signature = Hash.from_xml(signed_xml)['EntityDescriptor']['Signature']['SignatureValue']
+      signed_xml.gsub!(old_signature, 'sabotage')
+      subject = described_class.new(signed_xml)
+      expect(subject).to_not be_valid
+      expect(subject.errors[:signature]).to be_present
+    end
+
+    [
+      'ad_2012.xml',
+      'ad_with_logout.xml',
+      'okta.xml',
+    ].each do |xml_file|
+      it "validates #{xml_file}" do
+        travel_to DateTime.new(2017, 10, 6)
+        xml = IO.read("spec/fixtures/metadata/#{xml_file}")
+        expect(described_class.new(xml)).to be_valid
+      end
+    end
+  end
+end