Commit 051551d

mo <mo@mokhan.ca>
2017-11-15 02:52:10
drop header and checksum from compression and work only compressing for http-redirect.
1 parent 542ce4e
lib/saml/kit/authentication_request.rb
@@ -24,6 +24,10 @@ module Saml
         to_h[name]['ID']
       end
 
+      def destination
+        to_h[name]['Destination']
+      end
+
       def acs_url
         to_h[name]['AssertionConsumerServiceURL'] || registered_acs_url
       end
@@ -128,8 +132,7 @@ module Saml
       end
 
       class Builder
-        attr_accessor :id, :now, :issuer, :acs_url, :name_id_format
-        attr_reader :sign
+        attr_accessor :id, :now, :issuer, :acs_url, :name_id_format, :sign, :destination
 
         def initialize(user = nil, configuration: Saml::Kit.configuration, sign: true)
           @id = SecureRandom.uuid
@@ -162,6 +165,7 @@ module Saml
             ID: "_#{id}",
             Version: "2.0",
             IssueInstant: now.utc.iso8601,
+            Destination: destination,
           }
           options[:AssertionConsumerServiceURL] = acs_url if acs_url.present?
           options
lib/saml/kit/content.rb
@@ -24,7 +24,7 @@ module Saml
       end
 
       def self.encode(value)
-        Base64.strict_encode64(value).gsub(/\n/, '')
+        Base64.strict_encode64(value)
       end
 
       def self.base64_encoded?(value)
@@ -32,11 +32,12 @@ module Saml
       end
 
       def self.inflate(value)
-        Zlib::Inflate.new.inflate(value)
+        inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
+        inflater.inflate(value)
       end
 
       def self.deflate(value, level: Zlib::BEST_COMPRESSION)
-        Zlib::Deflate.deflate(value, level)
+        Zlib::Deflate.deflate(value, level)[2..-5]
       end
     end
   end
lib/saml/kit/namespaces.rb
@@ -6,6 +6,7 @@ module Saml
       BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
       BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
       EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
+      ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
       HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
       METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
       PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
@@ -13,13 +14,21 @@ module Saml
       PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
       POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
       PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
-      SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
       REQUESTER_ERROR = "urn:oasis:names:tc:SAML:2.0:status:Requester"
       RESPONDER_ERROR = "urn:oasis:names:tc:SAML:2.0:status:Responder"
-      VERSION_MISMATCH_ERROR = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"
+      RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
+      RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
+      RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
+      RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
+      SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
+      SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
+      SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
+      SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
+      SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
       TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
       UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:consent:unspecified"
       URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
+      VERSION_MISMATCH_ERROR = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"
       XMLDSIG = "http://www.w3.org/2000/09/xmldsig#"
 
       def self.binding_for(binding)
lib/saml/kit/url_builder.rb
@@ -0,0 +1,27 @@
+module Saml
+  module Kit
+    class UrlBuilder
+      def build(request, binding:, relay_state: nil)
+        payload = {
+          'SAMLRequest' => Content.encode_raw_saml(request.to_xml),
+          'RelayState' => relay_state,
+          'SigAlg' => Saml::Kit::Namespaces::SHA256,
+        }.map do |(x, y)|
+          "#{x}=#{y}"
+        end.join('&')
+        payload = URI.encode(payload)
+        "#{request.destination}?#{payload}&Signature=#{signature_for(payload)}"
+      end
+
+      private
+
+      def private_key
+        Saml::Kit.configuration.signing_private_key
+      end
+
+      def signature_for(payload)
+        Base64.strict_encode64(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
+      end
+    end
+  end
+end
lib/saml/kit.rb
@@ -29,6 +29,7 @@ require "saml/kit/invalid_request"
 require "saml/kit/self_signed_certificate"
 require "saml/kit/service_provider_metadata"
 require "saml/kit/signature"
+require "saml/kit/url_builder"
 require "saml/kit/xml"
 
 I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
spec/saml/authentication_request_spec.rb
@@ -5,6 +5,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
   let(:id) { SecureRandom.uuid }
   let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
   let(:issuer) { FFaker::Movie.title }
+  let(:destination) { FFaker::Internet.http_url }
   let(:name_id_format) { Saml::Kit::Namespaces::EMAIL_ADDRESS }
   let(:raw_xml) do
     builder = described_class::Builder.new
@@ -13,6 +14,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
     builder.issuer = issuer
     builder.acs_url = acs_url
     builder.name_id_format = name_id_format
+    builder.destination = destination
     builder.to_xml
   end
 
@@ -20,6 +22,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
   it { expect(subject.id).to eql("_#{id}") }
   it { expect(subject.acs_url).to eql(acs_url) }
   it { expect(subject.name_id_format).to eql(name_id_format) }
+  it { expect(subject.destination).to eql(destination) }
 
   describe "#to_xml" do
     subject { described_class::Builder.new(configuration: configuration) }
@@ -175,12 +178,10 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
   end
 
   describe "#serialize" do
-    it 'returns a compressed and base64 encoded document' do
-      builder = described_class::Builder.new
-      xml = builder.to_xml
-      subject = described_class.new(xml)
+    subject { described_class::Builder.new.build }
 
-      expected_value = Base64.encode64(Zlib::Deflate.deflate(xml, 9)).gsub(/\n/, '')
+    it 'returns a compressed and base64 encoded document' do
+      expected_value = Base64.strict_encode64(Zlib::Deflate.deflate(subject.to_xml, 9)[2..-5])
       expect(subject.serialize).to eql(expected_value)
     end
   end
spec/saml/logout_response_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Saml::Kit::LogoutResponse do
     subject { builder.build }
 
     it 'returns a compressed and base64 encoded document' do
-      expected_value = Base64.encode64(Zlib::Deflate.deflate(subject.to_xml, 9)).gsub(/\n/, '')
+      expected_value = Base64.strict_encode64(Zlib::Deflate.deflate(subject.to_xml, 9)[2..-5])
       expect(subject.serialize).to eql(expected_value)
     end
   end
spec/saml/response_spec.rb
@@ -244,7 +244,7 @@ RSpec.describe Saml::Kit::Response do
       xml = builder.to_xml
       subject = described_class.new(xml)
 
-      expected_value = Base64.encode64(Zlib::Deflate.deflate(xml, 9)).gsub(/\n/, '')
+      expected_value = Base64.strict_encode64(Zlib::Deflate.deflate(xml, 9)[2..-5])
       expect(subject.serialize).to eql(expected_value)
     end
   end
spec/saml/url_builder_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::UrlBuilder do
+  describe "#build" do
+    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, destination: destination, to_xml: xml) }
+    let(:xml) { "<xml></xml>" }
+    let(:destination) { FFaker::Internet.http_url }
+    let(:relay_state) { FFaker::Movie.title }
+    let(:query_params) { Hash[result_uri.query.split("&").map { |x| x.split('=', 2) }] }
+    let(:result) { subject.build(request, binding: :http_redirect, relay_state: relay_state) }
+    let(:result_uri) { URI.parse(result) }
+
+    it 'returns a url containing the target location' do
+      expect(result_uri.scheme).to eql("http")
+      expect(result_uri.host).to eql(URI.parse(destination).host)
+    end
+
+    it 'includes the message deflated (without header and checksum), base64-encoded, and URL-encoded' do
+      level = Zlib::BEST_COMPRESSION
+      expected = URI.encode(Base64.encode64(Zlib::Deflate.deflate(xml, level)[2..-5]).gsub(/\n/, ''))
+      expect(result).to include("SAMLRequest=#{expected}")
+      expect(query_params['SAMLRequest']).to eql(expected)
+    end
+
+    it 'includes the relay state' do
+      expect(query_params['RelayState']).to eql(URI.encode(relay_state))
+      expect(result).to include("RelayState=#{URI.encode(relay_state)}")
+    end
+
+<<-DOC
+2. To construct the signature, a string consisting of the concatenation of the RelayState 
+(if present), SigAlg, and SAMLRequest (or SAMLResponse) query string parameters 
+(each one URLencoded) is constructed in one of the following ways (ordered as below):
+
+SAMLRequest=value&RelayState=value&SigAlg=value
+SAMLResponse=value&RelayState=value&SigAlg=value
+
+3. The resulting string of bytes is the octet string to be fed into the signature algorithm.
+Any other content in the original query string is not included and not signed.
+4. The signature value MUST be encoded using the base64 encoding (see RFC 2045 [RFC2045]) with
+any whitespace removed, and included as a query string parameter named Signature.
+DOC
+    it 'includes a signature' do
+      expect(query_params['SigAlg']).to eql(URI.encode(Saml::Kit::Namespaces::SHA256))
+
+      payload = "SAMLRequest=#{query_params['SAMLRequest']}"
+      payload << "&RelayState=#{query_params['RelayState']}"
+      payload << "&SigAlg=#{query_params['SigAlg']}"
+      expected_signature = Base64.strict_encode64(Saml::Kit.configuration.signing_private_key.sign(OpenSSL::Digest::SHA256.new, payload))
+      expect(query_params['Signature']).to eql(expected_signature)
+    end
+  end
+end