Commit 7d61e4d
Changed files (5)
lib
spec
saml
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