Commit af87b45

mo <mo@mokhan.ca>
2017-11-03 22:00:13
validate certificate matches registered service provider.
1 parent de6f712
lib/saml/kit/authentication_request.rb
@@ -5,6 +5,7 @@ module Saml
       validates_presence_of :content
       validate :must_be_request
       validate :must_have_valid_signature
+      validate :must_be_registered_service_provider
 
       attr_reader :content, :name
 
@@ -26,6 +27,14 @@ module Saml
         @hash[name]['Issuer']
       end
 
+      def certificate
+        @hash[name]['Signature']['KeyInfo']['X509Data']['X509Certificate']
+      end
+
+      def fingerprint
+        Fingerprint.new(certificate)
+      end
+
       def to_xml
         @content
       end
@@ -36,6 +45,13 @@ module Saml
 
       private
 
+      def must_be_registered_service_provider
+        return unless login_request?
+        return if Saml::Kit.configuration.service_provider_registry.registered?(issuer, fingerprint)
+
+        errors[:base] << error_message(:invalid)
+      end
+
       def must_have_valid_signature
         return if to_xml.blank?
 
@@ -49,9 +65,12 @@ module Saml
       def must_be_request
         return if @hash.nil?
 
-        if @hash[name].blank?
-          errors[:base] << error_message(:invalid)
-        end
+        errors[:base] << error_message(:invalid) unless login_request?
+      end
+
+      def login_request?
+        return false if to_xml.blank?
+        @hash[name].present?
       end
 
       def error_message(key)
lib/saml/kit/configuration.rb
@@ -7,11 +7,15 @@ 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 :service_provider_registry
 
       def initialize
+        #@issuer = SecureRandom.uuid
         @signature_method = :SHA256
         @digest_method = :SHA256
-        @signing_certificate_pem, @signing_private_key_pem, @signing_private_key_password = create_self_signed_certificate
+        @signing_private_key_password = SecureRandom.uuid
+        @signing_certificate_pem, @signing_private_key_pem = SelfSignedCertificate.new(@signing_private_key_password).create
+        @service_provider_registry = DefaultServiceProviderRegistry.new
       end
 
       def stripped_signing_certificate
@@ -25,32 +29,6 @@ module Saml
       def signing_private_key
         OpenSSL::PKey::RSA.new(signing_private_key_pem, signing_private_key_password)
       end
-
-      private
-
-      def create_self_signed_certificate
-        rsa_key = OpenSSL::PKey::RSA.new(2048)
-        public_key = rsa_key.public_key
-        certificate = OpenSSL::X509::Certificate.new
-        certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse("/C=CA/ST=Alberta/L=Calgary/O=Xsig/OU=Xsig/CN=Xsig")
-        certificate.not_before = DateTime.now.beginning_of_day
-        certificate.not_after = 1.year.from_now.end_of_day
-        certificate.public_key = public_key
-        certificate.serial = 0x0
-        certificate.version = 2
-        factory = OpenSSL::X509::ExtensionFactory.new
-        factory.subject_certificate = factory.issuer_certificate = certificate
-        certificate.extensions = [ factory.create_extension("basicConstraints","CA:TRUE", true), factory.create_extension("subjectKeyIdentifier", "hash"), ]
-        certificate.add_extension(factory.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always"))
-        certificate.sign(rsa_key, OpenSSL::Digest::SHA256.new)
-
-        password = SecureRandom.uuid
-        [
-          certificate.to_pem,
-          rsa_key.to_pem(OpenSSL::Cipher::Cipher.new('des3'), password),
-          password
-        ]
-      end
     end
   end
 end
lib/saml/kit/default_service_provider_registry.rb
@@ -0,0 +1,9 @@
+module Saml
+  module Kit
+    class DefaultServiceProviderRegistry
+      def registered?(issuer, fingerprint)
+        issuer.present? && fingerprint.algorithm(OpenSSL::Digest::SHA256).present?
+      end
+    end
+  end
+end
lib/saml/kit/fingerprint.rb
@@ -0,0 +1,31 @@
+module Saml
+  module Kit
+    class Fingerprint
+      attr_reader :x509
+
+      def initialize(raw_certificate)
+        @x509 = OpenSSL::X509::Certificate.new(raw_certificate)
+      rescue OpenSSL::X509::CertificateError
+        @x509 = OpenSSL::X509::Certificate.new(Base64.decode64(raw_certificate))
+      end
+
+      def algorithm(algorithm)
+        pretty_fingerprint(algorithm.new.hexdigest(x509.to_der))
+      end
+
+      def ==(other)
+        self.to_s == other.to_s
+      end
+
+      def to_s
+        algorithm(OpenSSL::Digest::SHA256)
+      end
+
+      private
+
+      def pretty_fingerprint(fingerprint)
+        fingerprint.upcase.scan(/../).join(":")
+      end
+    end
+  end
+end
lib/saml/kit/metadata.rb
@@ -37,7 +37,7 @@ module Saml
           cert = item.at_xpath("./ds:KeyInfo/ds:X509Data/ds:X509Certificate", NAMESPACES).text
           {
             text: cert,
-            fingerprint: fingerprint_for(cert, OpenSSL::Digest::SHA256),
+            fingerprint: Fingerprint.new(cert).algorithm(OpenSSL::Digest::SHA256),
             use: item.attribute('use').value,
           }
         end
@@ -79,15 +79,6 @@ module Saml
         document.search(xpath, NAMESPACES)
       end
 
-      def fingerprint_for(value, algorithm)
-        x509 = OpenSSL::X509::Certificate.new(Base64.decode64(value))
-        pretty_fingerprint(algorithm.new.hexdigest(x509.to_der))
-      end
-
-      def pretty_fingerprint(fingerprint)
-        fingerprint.upcase.scan(/../).join(":")
-      end
-
       def metadata
         find_by("/md:EntityDescriptor/md:#{descriptor_name}").present?
       end
lib/saml/kit/self_signed_certificate.rb
@@ -0,0 +1,30 @@
+module Saml
+  module Kit
+    class SelfSignedCertificate
+      def initialize(password)
+        @password = password
+      end
+
+      def create
+        rsa_key = OpenSSL::PKey::RSA.new(2048)
+        public_key = rsa_key.public_key
+        certificate = OpenSSL::X509::Certificate.new
+        certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse("/C=CA/ST=Alberta/L=Calgary/O=Xsig/OU=Xsig/CN=Xsig")
+        certificate.not_before = DateTime.now.beginning_of_day
+        certificate.not_after = 1.year.from_now.end_of_day
+        certificate.public_key = public_key
+        certificate.serial = 0x0
+        certificate.version = 2
+        factory = OpenSSL::X509::ExtensionFactory.new
+        factory.subject_certificate = factory.issuer_certificate = certificate
+        certificate.extensions = [ factory.create_extension("basicConstraints","CA:TRUE", true), factory.create_extension("subjectKeyIdentifier", "hash"), ]
+        certificate.add_extension(factory.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always"))
+        certificate.sign(rsa_key, OpenSSL::Digest::SHA256.new)
+        [
+          certificate.to_pem,
+          rsa_key.to_pem(OpenSSL::Cipher::Cipher.new('des3'), @password)
+        ]
+      end
+    end
+  end
+end
lib/saml/kit.rb
@@ -12,11 +12,14 @@ require "xmldsig"
 
 require "saml/kit/authentication_request"
 require "saml/kit/configuration"
+require "saml/kit/default_service_provider_registry"
+require "saml/kit/fingerprint"
 require "saml/kit/namespaces"
 require "saml/kit/metadata"
 require "saml/kit/request"
 require "saml/kit/response"
 require "saml/kit/identity_provider_metadata"
+require "saml/kit/self_signed_certificate"
 require "saml/kit/service_provider_metadata"
 require "saml/kit/signature"
 require "saml/kit/xml"
spec/saml/authentication_request_spec.rb
@@ -43,6 +43,8 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
   end
 
   describe "#valid?" do
+    let(:registry) { double }
+
     it 'is valid when left untampered' do
       expect(described_class.new(raw_xml)).to be_valid
     end
@@ -61,5 +63,16 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
       xml = Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
       expect(described_class.new(xml)).to be_invalid
     end
+
+    it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
+      builder = described_class::Builder.new
+      builder.issuer = issuer
+      xml = builder.to_xml
+
+      allow(Saml::Kit.configuration).to receive(:service_provider_registry).and_return(registry)
+      fingerprint = Saml::Kit::Fingerprint.new(Hash.from_xml(xml)['AuthnRequest']['Signature']['KeyInfo']['X509Data']['X509Certificate'])
+      allow(registry).to receive(:registered?).with(issuer, fingerprint).and_return(false)
+      expect(described_class.new(xml)).to be_invalid
+    end
   end
 end
spec/saml/fingerprint_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Fingerprint do
+  describe "#sha" do
+    it 'returns the SHA256' do
+      certificate, _ = Saml::Kit::SelfSignedCertificate.new("password").create
+      x509 = OpenSSL::X509::Certificate.new(certificate)
+      sha256 = OpenSSL::Digest::SHA256.new.hexdigest(x509.to_der).upcase.scan(/../).join(":")
+
+      expect(described_class.new(certificate).algorithm(OpenSSL::Digest::SHA256)).to eql(sha256)
+    end
+
+    it 'returns the SHA1' do
+      certificate, _ = Saml::Kit::SelfSignedCertificate.new("password").create
+      x509 = OpenSSL::X509::Certificate.new(certificate)
+      sha1 = OpenSSL::Digest::SHA1.new.hexdigest(x509.to_der).upcase.scan(/../).join(":")
+
+      expect(described_class.new(certificate).algorithm(OpenSSL::Digest::SHA1)).to eql(sha1)
+    end
+  end
+end