Commit 7d61e4d

mo <mo.khan@gmail.com>
2017-11-01 18:03:03
add a digital signature to the authnrequest.
1 parent 1d06541
lib/saml/kit/authentication_request.rb
@@ -42,6 +42,7 @@ module Saml
         end
 
         def to_xml(xml = ::Builder::XmlMarkup.new)
+          signature = Signature.new(id)
           xml.tag!('samlp:AuthnRequest',
                    "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
                    "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
@@ -50,10 +51,11 @@ module Saml
                    IssueInstant: issued_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
                    AssertionConsumerServiceURL: acs_url,
                   ) do
+            signature.template(xml)
             xml.tag!('saml:Issuer', issuer)
             xml.tag!('samlp:NameIDPolicy', Format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
           end
-          xml.target!
+          signature.finalize(xml)
         end
 
         def build
lib/saml/kit/configuration.rb
@@ -1,7 +1,52 @@
 module Saml
   module Kit
     class Configuration
+      BEGIN_CERT=/-----BEGIN CERTIFICATE-----/
+      END_CERT=/-----END CERTIFICATE-----/
+
       attr_accessor :issuer, :acs_url
+      attr_accessor :signature_method, :digest_method
+      attr_accessor :certificate_pem, :private_key_pem, :private_key_password
+
+      def initialize
+        @signature_method = :SHA256
+        @digest_method = :SHA256
+        @certificate_pem, @private_key_pem, @private_key_password = create_self_signed_certificate
+      end
+
+      def stripped_certificate
+        certificate_pem.to_s.gsub(BEGIN_CERT, '').gsub(END_CERT, '').gsub(/\n/, '')
+      end
+
+      def private_key
+        OpenSSL::PKey::RSA.new(private_key_pem, 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 = Time.now
+        certificate.not_after = Time.now + 365 * 24 * 60 * 60
+        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/signature.rb
@@ -0,0 +1,56 @@
+module Saml
+  module Kit
+    class Signature
+      XMLDSIG="http://www.w3.org/2000/09/xmldsig#"
+      SIGNATURE_METHODS = {
+        SHA1: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
+        SHA224: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224",
+        SHA256: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+        SHA384: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
+        SHA512: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
+      }.freeze
+      DIGEST_METHODS = {
+        SHA1: "http://www.w3.org/2000/09/xmldsig#SHA1",
+        SHA224: "http://www.w3.org/2001/04/xmldsig-more#sha224",
+        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",
+      }.freeze
+
+      attr_reader :configuration, :reference_id
+
+      def initialize(reference_id, configuration = Saml::Kit.configuration)
+        @reference_id = reference_id
+        @configuration = configuration
+      end
+
+      def template(xml = ::Builder::XmlMarkup.new)
+        xml.tag! "ds:Signature", "xmlns:ds" => XMLDSIG do
+          xml.tag! "ds:SignedInfo", "xmlns:ds" => XMLDSIG do
+            xml.tag! "ds:CanonicalizationMethod", Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#"
+            xml.tag! "ds:SignatureMethod", Algorithm: SIGNATURE_METHODS[configuration.signature_method]
+            xml.tag! "ds:Reference", URI: "##{reference_id}" do
+              xml.tag! "ds:Transforms" do
+                xml.tag! "ds:Transform", Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
+                xml.tag! "ds:Transform", Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#"
+              end
+              xml.tag! "ds:DigestMethod", Algorithm: DIGEST_METHODS[configuration.digest_method]
+              xml.tag! "ds:DigestValue", ""
+            end
+          end
+          xml.tag! "ds:SignatureValue", ""
+          xml.tag! "ds:KeyInfo" do
+            xml.tag! "ds:X509Data" do
+              xml.tag! "ds:X509Certificate", configuration.stripped_certificate
+            end
+          end
+        end
+      end
+
+      def finalize(xml)
+        document = Xmldsig::SignedDocument.new(xml.target!)
+        document.sign(configuration.private_key)
+      end
+    end
+  end
+end
lib/saml/kit.rb
@@ -17,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/signature"
 require "saml/kit/xml"
 
 I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
spec/saml/signature_spec.rb
@@ -0,0 +1,65 @@
+require "spec_helper"
+
+RSpec.describe Saml::Kit::Signature do
+  subject { described_class.new(reference_id, configuration) }
+  let(:configuration) do
+    config = Saml::Kit::Configuration.new
+    config.certificate_pem = certificate
+    config.private_key_pem = private_key
+    config.private_key_password = password
+    config
+  end
+
+  let(:reference_id) { SecureRandom.uuid }
+  let(:rsa_key) { OpenSSL::PKey::RSA.new(2048) }
+  let(:public_key) { rsa_key.public_key }
+  let(:certificate) do
+    x = OpenSSL::X509::Certificate.new
+    x.subject = x.issuer = OpenSSL::X509::Name.parse("/C=CA/ST=Alberta/L=Calgary/O=Xsig/OU=Xsig/CN=Xsig")
+    x.not_before = Time.now
+    x.not_after = Time.now + 365 * 24 * 60 * 60
+    x.public_key = public_key
+    x.serial = 0x0
+    x.version = 2
+    factory = OpenSSL::X509::ExtensionFactory.new
+    factory.subject_certificate = factory.issuer_certificate = x
+    x.extensions = [ factory.create_extension("basicConstraints","CA:TRUE", true), factory.create_extension("subjectKeyIdentifier", "hash"), ]
+    x.add_extension(factory.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always"))
+    x.sign(rsa_key, OpenSSL::Digest::SHA256.new)
+    x.to_pem
+  end
+  let(:private_key) { rsa_key.to_pem(OpenSSL::Cipher::Cipher.new('des3'), password) }
+  let(:password) { "password" }
+
+  it 'generates a signature' do
+    xml = ::Builder::XmlMarkup.new
+    options = {
+      "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
+      "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
+      ID: "#{reference_id}",
+    }
+    xml.tag!('samlp:AuthnRequest', options) do
+      subject.template(xml)
+      xml.tag!('saml:Issuer', "MyEntityID")
+    end
+    result = Hash.from_xml(subject.finalize(xml))
+
+    signature = result["AuthnRequest"]["Signature"]
+    expect(signature['xmlns:ds']).to eql("http://www.w3.org/2000/09/xmldsig#")
+    expect(signature['SignedInfo']['xmlns:ds']).to eql("http://www.w3.org/2000/09/xmldsig#")
+    expect(signature['SignedInfo']['CanonicalizationMethod']['Algorithm']).to eql('http://www.w3.org/2001/10/xml-exc-c14n#')
+    expect(signature['SignedInfo']['SignatureMethod']['Algorithm']).to eql("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
+
+    expect(signature['SignedInfo']['Reference']['URI']).to eql("##{reference_id}")
+    expect(signature['SignedInfo']['Reference']['Transforms']['Transform']).to match_array([
+      { "Algorithm" => "http://www.w3.org/2000/09/xmldsig#enveloped-signature" },
+      { "Algorithm" => "http://www.w3.org/2001/10/xml-exc-c14n#" }
+    ])
+    expect(signature['SignedInfo']['Reference']['DigestMethod']['Algorithm']).to eql("http://www.w3.org/2001/04/xmlenc#sha256")
+    expected_certificate = certificate.gsub(/\n/, '').gsub(/-----BEGIN CERTIFICATE-----/, '').gsub(/-----END CERTIFICATE-----/, '')
+    expect(signature['KeyInfo']['X509Data']['X509Certificate']).to eql(expected_certificate)
+    expect(signature['SignedInfo']['Reference']['DigestValue']).to be_present
+    expect(signature['SignatureValue']).to be_present
+    expect(OpenSSL::X509::Certificate.new(Base64.decode64(signature['KeyInfo']['X509Data']['X509Certificate']))).to be_present
+  end
+end