samlr 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of samlr might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +8 -0
- data/LICENSE +176 -0
- data/README.md +182 -0
- data/Rakefile +12 -0
- data/bin/samlr +46 -0
- data/config/schemas/XMLSchema.xsd +2534 -0
- data/config/schemas/saml-schema-assertion-2.0.xsd +283 -0
- data/config/schemas/saml-schema-metadata-2.0.xsd +337 -0
- data/config/schemas/saml-schema-protocol-2.0.xsd +302 -0
- data/config/schemas/xenc-schema.xsd +146 -0
- data/config/schemas/xml.xsd +287 -0
- data/config/schemas/xmldsig-core-schema.xsd +318 -0
- data/lib/samlr.rb +52 -0
- data/lib/samlr/assertion.rb +91 -0
- data/lib/samlr/certificate.rb +23 -0
- data/lib/samlr/command.rb +41 -0
- data/lib/samlr/condition.rb +31 -0
- data/lib/samlr/errors.rb +22 -0
- data/lib/samlr/fingerprint.rb +44 -0
- data/lib/samlr/logout_request.rb +7 -0
- data/lib/samlr/reference.rb +32 -0
- data/lib/samlr/request.rb +37 -0
- data/lib/samlr/response.rb +68 -0
- data/lib/samlr/signature.rb +129 -0
- data/lib/samlr/tools.rb +108 -0
- data/lib/samlr/tools/certificate_builder.rb +74 -0
- data/lib/samlr/tools/logout_request_builder.rb +27 -0
- data/lib/samlr/tools/metadata_builder.rb +41 -0
- data/lib/samlr/tools/request_builder.rb +44 -0
- data/lib/samlr/tools/response_builder.rb +157 -0
- data/lib/samlr/tools/timestamp.rb +26 -0
- data/samlr.gemspec +19 -0
- data/test/fixtures/default_samlr_certificate.pem +11 -0
- data/test/fixtures/default_samlr_private_key.pem +9 -0
- data/test/fixtures/no_cert_response.xml +2 -0
- data/test/fixtures/sample_metadata.xml +7 -0
- data/test/fixtures/sample_response.xml +2 -0
- data/test/test_helper.rb +55 -0
- data/test/unit/test_assertion.rb +54 -0
- data/test/unit/test_condition.rb +71 -0
- data/test/unit/test_fingerprint.rb +45 -0
- data/test/unit/test_logout_request.rb +39 -0
- data/test/unit/test_reference.rb +32 -0
- data/test/unit/test_request.rb +34 -0
- data/test/unit/test_response.rb +94 -0
- data/test/unit/test_response_scenarios.rb +111 -0
- data/test/unit/test_signature.rb +54 -0
- data/test/unit/test_timestamp.rb +58 -0
- data/test/unit/test_tools.rb +100 -0
- data/test/unit/tools/test_certificate_builder.rb +41 -0
- data/test/unit/tools/test_logout_request_builder.rb +26 -0
- data/test/unit/tools/test_metadata_builder.rb +26 -0
- data/test/unit/tools/test_request_builder.rb +35 -0
- data/test/unit/tools/test_response_builder.rb +19 -0
- metadata +184 -0
    
        data/lib/samlr/tools.rb
    ADDED
    
    | @@ -0,0 +1,108 @@ | |
| 1 | 
            +
            require "time"
         | 
| 2 | 
            +
            require "uuidtools"
         | 
| 3 | 
            +
            require "openssl"
         | 
| 4 | 
            +
            require "cgi"
         | 
| 5 | 
            +
            require "zlib"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require "samlr/tools/timestamp"
         | 
| 8 | 
            +
            require "samlr/tools/certificate_builder"
         | 
| 9 | 
            +
            require "samlr/tools/request_builder"
         | 
| 10 | 
            +
            require "samlr/tools/response_builder"
         | 
| 11 | 
            +
            require "samlr/tools/metadata_builder"
         | 
| 12 | 
            +
            require "samlr/tools/logout_request_builder"
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            module Samlr
         | 
| 15 | 
            +
              module Tools
         | 
| 16 | 
            +
                SHA_MAP = {
         | 
| 17 | 
            +
                  1    => OpenSSL::Digest::SHA1,
         | 
| 18 | 
            +
                  256  => OpenSSL::Digest::SHA256,
         | 
| 19 | 
            +
                  384  => OpenSSL::Digest::SHA384,
         | 
| 20 | 
            +
                  512  => OpenSSL::Digest::SHA512
         | 
| 21 | 
            +
                }
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                # Convert algorithm attribute value to Ruby implementation
         | 
| 24 | 
            +
                def self.algorithm(value)
         | 
| 25 | 
            +
                  if value =~ /sha(\d+)$/
         | 
| 26 | 
            +
                    implementation = SHA_MAP[$1.to_i]
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  implementation || OpenSSL::Digest::SHA1
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                # Accepts a document and optionally :path => xpath, :c14n_mode => c14n_mode
         | 
| 33 | 
            +
                def self.canonicalize(xml, options = {})
         | 
| 34 | 
            +
                  options  = { :c14n_mode => C14N }.merge(options)
         | 
| 35 | 
            +
                  document = Nokogiri::XML(xml) { |c| c.strict.noblanks }
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  if path = options[:path]
         | 
| 38 | 
            +
                    node = document.at(path, NS_MAP)
         | 
| 39 | 
            +
                  else
         | 
| 40 | 
            +
                    node = document
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  node.canonicalize(options[:c14n_mode], options[:namespaces])
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                # Generate an xs:NCName conforming UUID
         | 
| 47 | 
            +
                def self.uuid
         | 
| 48 | 
            +
                  "samlr-#{UUIDTools::UUID.timestamp_create}"
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                # Deflates, Base64 encodes and CGI escapes a string
         | 
| 52 | 
            +
                def self.encode(string)
         | 
| 53 | 
            +
                  deflated = Zlib::Deflate.deflate(string, 9)[2..-5]
         | 
| 54 | 
            +
                  encoded  = Base64.encode64(deflated)
         | 
| 55 | 
            +
                  escaped  = CGI.escape(encoded)
         | 
| 56 | 
            +
                  escaped
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                # CGI unescapes, Base64 decodes and inflates a string
         | 
| 60 | 
            +
                def self.decode(string)
         | 
| 61 | 
            +
                  unescaped = CGI.unescape(string)
         | 
| 62 | 
            +
                  decoded   = Base64.decode64(unescaped)
         | 
| 63 | 
            +
                  inflater  = Zlib::Inflate.new(-Zlib::MAX_WBITS)
         | 
| 64 | 
            +
                  inflated  = inflater.inflate(decoded)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  inflater.finish
         | 
| 67 | 
            +
                  inflater.close
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  inflated
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def self.validate!(options = {})
         | 
| 73 | 
            +
                  validate(options.merge(:bang => true))
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                # Validate a SAML request or response against an XSD. Supply either :path or :document in the options and
         | 
| 77 | 
            +
                # a :schema (defaults to SAML validation)
         | 
| 78 | 
            +
                def self.validate(options = {})
         | 
| 79 | 
            +
                  document = options[:document] || File.read(options[:path])
         | 
| 80 | 
            +
                  schema   = options.fetch(:schema, SAML_SCHEMA)
         | 
| 81 | 
            +
                  bang     = options.fetch(:bang, false)
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  if document.is_a?(Nokogiri::XML::Document)
         | 
| 84 | 
            +
                    xml = document
         | 
| 85 | 
            +
                  else
         | 
| 86 | 
            +
                    xml = Nokogiri::XML(document) { |c| c.strict }
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  # All bundled schemas are using relative schemaLocation. This means we'll have to
         | 
| 90 | 
            +
                  # change working directory to find them during validation.
         | 
| 91 | 
            +
                  Dir.chdir(Samlr.schema_location) do
         | 
| 92 | 
            +
                    if schema.is_a?(Nokogiri::XML::Schema)
         | 
| 93 | 
            +
                      xsd = schema
         | 
| 94 | 
            +
                    else
         | 
| 95 | 
            +
                      xsd = Nokogiri::XML::Schema(File.read(schema))
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                    result = xsd.validate(xml)
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                    if bang && result.length != 0
         | 
| 101 | 
            +
                      raise Samlr::FormatError.new("Schema validation failed", "XSD validation errors: #{result.join(", ")}")
         | 
| 102 | 
            +
                    else
         | 
| 103 | 
            +
                      result.length == 0
         | 
| 104 | 
            +
                    end
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
              end
         | 
| 108 | 
            +
            end
         | 
| @@ -0,0 +1,74 @@ | |
| 1 | 
            +
            module Samlr
         | 
| 2 | 
            +
              module Tools
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                # Container for generating/referencing X509 and keys
         | 
| 5 | 
            +
                class CertificateBuilder
         | 
| 6 | 
            +
                  attr_reader :key_size
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(options = {})
         | 
| 9 | 
            +
                    @key_size = options.fetch(:key_size, 4096)
         | 
| 10 | 
            +
                    @x509     = options[:x509]
         | 
| 11 | 
            +
                    @key_pair = options[:key_pair]
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def x509
         | 
| 15 | 
            +
                    @x509 ||= begin
         | 
| 16 | 
            +
                      domain = "example.org"
         | 
| 17 | 
            +
                      name   = OpenSSL::X509::Name.new([
         | 
| 18 | 
            +
                        [ 'C', 'US', OpenSSL::ASN1::PRINTABLESTRING ],
         | 
| 19 | 
            +
                        [ 'O', domain, OpenSSL::ASN1::UTF8STRING ],
         | 
| 20 | 
            +
                        [ 'OU', 'Samlr ResponseBuilder', OpenSSL::ASN1::UTF8STRING ],
         | 
| 21 | 
            +
                        [ 'CN', 'CA' ]
         | 
| 22 | 
            +
                        ])
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                      certificate = OpenSSL::X509::Certificate.new
         | 
| 25 | 
            +
                      certificate.subject    = name
         | 
| 26 | 
            +
                      certificate.issuer     = name
         | 
| 27 | 
            +
                      certificate.not_before = (Time.now - 5)
         | 
| 28 | 
            +
                      certificate.not_after  = (Time.now + 60 * 60 * 24 * 365 * 20)
         | 
| 29 | 
            +
                      certificate.public_key = key_pair.public_key
         | 
| 30 | 
            +
                      certificate.serial     = 1
         | 
| 31 | 
            +
                      certificate.version    = 2
         | 
| 32 | 
            +
                      certificate.sign(key_pair, OpenSSL::Digest::SHA1.new)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                      certificate
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def x509_as_pem
         | 
| 39 | 
            +
                    pem = x509.to_pem.split("\n")
         | 
| 40 | 
            +
                    pem.pop
         | 
| 41 | 
            +
                    pem.shift
         | 
| 42 | 
            +
                    pem.join
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  def key_pair
         | 
| 46 | 
            +
                    @key_pair ||= OpenSSL::PKey::RSA.new(key_size)
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def sign(string)
         | 
| 50 | 
            +
                    Base64.encode64(key_pair.sign(OpenSSL::Digest::SHA1.new, string)).delete("\n")
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def verify(signature, string)
         | 
| 54 | 
            +
                    key_pair.public_key.verify(OpenSSL::Digest::SHA1.new, Base64.decode64(signature), string)
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  def to_certificate
         | 
| 58 | 
            +
                    Samlr::Certificate.new(x509)
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def self.dump(path, certificate, id = "samlr")
         | 
| 62 | 
            +
                    File.open(File.join(path, "#{id}_private_key.pem"), "w") { |f| f.write(certificate.key_pair.to_pem) }
         | 
| 63 | 
            +
                    File.open(File.join(path, "#{id}_certificate.pem"), "w") { |f| f.write(certificate.x509.to_pem) }
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  def self.load(path, id = "samlr")
         | 
| 67 | 
            +
                    key_pair  = OpenSSL::PKey::RSA.new(File.read(File.join(path, "#{id}_private_key.pem")))
         | 
| 68 | 
            +
                    x509_cert = OpenSSL::X509::Certificate.new(File.read(File.join(path, "#{id}_certificate.pem")))
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    new(:key_pair => key_pair, :x509 => x509_cert)
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
            end
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            require "nokogiri"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Samlr
         | 
| 4 | 
            +
              module Tools
         | 
| 5 | 
            +
                # Use this for building the SAML logout request XML
         | 
| 6 | 
            +
                module LogoutRequestBuilder
         | 
| 7 | 
            +
                  def self.build(options = {})
         | 
| 8 | 
            +
                    name_id_format  = options[:name_id_format] || EMAIL_FORMAT
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    # Mandatory
         | 
| 11 | 
            +
                    name_id = options.fetch(:name_id)
         | 
| 12 | 
            +
                    issuer  = options.fetch(:issuer)
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    builder = Nokogiri::XML::Builder.new do |xml|
         | 
| 15 | 
            +
                      xml.LogoutRequest("xmlns:samlp" => NS_MAP["samlp"], "xmlns:saml" => NS_MAP["saml"], "ID" => Samlr::Tools.uuid, "IssueInstant" => Samlr::Tools::Timestamp.stamp, "Version" => "2.0") do
         | 
| 16 | 
            +
                        xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "samlp" }
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                        xml["saml"].Issuer(issuer)
         | 
| 19 | 
            +
                        xml["saml"].NameID(name_id, "Format" => name_id_format)
         | 
| 20 | 
            +
                      end
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    builder.to_xml(COMPACT)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,41 @@ | |
| 1 | 
            +
            module Samlr
         | 
| 2 | 
            +
              module Tools
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                # Builds you some SP metadata. Accepts a hash with the below keys. No support for arrays
         | 
| 5 | 
            +
                # of name id formats or asserion consumer services, build it if you need it.
         | 
| 6 | 
            +
                #
         | 
| 7 | 
            +
                #  :entity_id            => "https://sp.example.org/saml", # mandatory
         | 
| 8 | 
            +
                #  :name_identity_format => Samlr::EMAIL_FORMAT,
         | 
| 9 | 
            +
                #  :consumer_service_url => "https://sp.example.org/saml"
         | 
| 10 | 
            +
                class MetadataBuilder
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def self.build(options = {})
         | 
| 13 | 
            +
                    name_identity_format     = options[:name_identity_format]
         | 
| 14 | 
            +
                    consumer_service_url     = options[:consumer_service_url]
         | 
| 15 | 
            +
                    consumer_service_binding = options[:consumer_service_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    # Mandatory
         | 
| 18 | 
            +
                    entity_id                 = options.fetch(:entity_id)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    builder = Nokogiri::XML::Builder.new do |xml|
         | 
| 21 | 
            +
                      xml.EntityDescriptor("xmlns:md" => NS_MAP["md"], "entityID" => entity_id) do
         | 
| 22 | 
            +
                        xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "md" }
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                        xml["md"].SPSSODescriptor("protocolSupportEnumeration" => NS_MAP["samlp"]) do
         | 
| 25 | 
            +
                          unless name_identity_format.nil?
         | 
| 26 | 
            +
                            xml["md"].NameIDFormat(name_identity_format)
         | 
| 27 | 
            +
                          end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                          unless consumer_service_url.nil?
         | 
| 30 | 
            +
                            xml["md"].AssertionConsumerService("index" => "0", "Binding" => consumer_service_binding, "Location" => consumer_service_url)
         | 
| 31 | 
            +
                          end
         | 
| 32 | 
            +
                        end
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    builder.to_xml(COMPACT)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            require "nokogiri"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Samlr
         | 
| 4 | 
            +
              module Tools
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                # Use this for building the SAML auth request XML
         | 
| 7 | 
            +
                module RequestBuilder
         | 
| 8 | 
            +
                  def self.build(options = {})
         | 
| 9 | 
            +
                    consumer_service_url = options[:consumer_service_url]
         | 
| 10 | 
            +
                    issuer               = options[:issuer]
         | 
| 11 | 
            +
                    name_identity_format = options[:name_identity_format]
         | 
| 12 | 
            +
                    allow_create         = options[:allow_create] || "true"
         | 
| 13 | 
            +
                    authn_context        = options[:authn_context]
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    builder = Nokogiri::XML::Builder.new do |xml|
         | 
| 16 | 
            +
                      xml.AuthnRequest("xmlns:samlp" => NS_MAP["samlp"], "xmlns:saml" => NS_MAP["saml"], "ID" => Samlr::Tools.uuid, "IssueInstant" => Samlr::Tools::Timestamp.stamp, "Version" => "2.0") do
         | 
| 17 | 
            +
                        xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "samlp" }
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                        unless consumer_service_url.nil?
         | 
| 20 | 
            +
                          xml.doc.root["AssertionConsumerServiceURL"] = consumer_service_url
         | 
| 21 | 
            +
                        end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                        unless issuer.nil?
         | 
| 24 | 
            +
                          xml["saml"].Issuer(issuer)
         | 
| 25 | 
            +
                        end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                        unless name_identity_format.nil?
         | 
| 28 | 
            +
                          xml["samlp"].NameIDPolicy("AllowCreate" => allow_create, "Format" => name_identity_format)
         | 
| 29 | 
            +
                        end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                        unless authn_context.nil?
         | 
| 32 | 
            +
                          xml["samlp"].RequestedAuthnContext("Comparison" => "exact") do
         | 
| 33 | 
            +
                            xml["saml"].AuthnContextClassRef(authn_context)
         | 
| 34 | 
            +
                          end
         | 
| 35 | 
            +
                        end
         | 
| 36 | 
            +
                      end
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    builder.to_xml(COMPACT)
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| @@ -0,0 +1,157 @@ | |
| 1 | 
            +
            require "nokogiri"
         | 
| 2 | 
            +
            require "time"
         | 
| 3 | 
            +
            require "uuidtools"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Samlr
         | 
| 6 | 
            +
              module Tools
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                # Use this for building test data, not ready to use for production data
         | 
| 9 | 
            +
                module ResponseBuilder
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def self.build(options = {})
         | 
| 12 | 
            +
                    issue_instant   = options[:issue_instant]  || Samlr::Tools::Timestamp.stamp
         | 
| 13 | 
            +
                    response_id     = options[:response_id]    || Samlr::Tools.uuid
         | 
| 14 | 
            +
                    assertion_id    = options[:assertion_id]   || Samlr::Tools.uuid
         | 
| 15 | 
            +
                    status_code     = options[:status_code]    || "urn:oasis:names:tc:SAML:2.0:status:Success"
         | 
| 16 | 
            +
                    name_id_format  = options[:name_id_format] || EMAIL_FORMAT
         | 
| 17 | 
            +
                    subject_conf_m  = options[:subject_conf_m] || "urn:oasis:names:tc:SAML:2.0:cm:bearer"
         | 
| 18 | 
            +
                    version         = options[:version]        || "2.0"
         | 
| 19 | 
            +
                    auth_context    = options[:auth_context]   || "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
         | 
| 20 | 
            +
                    issuer          = options[:issuer]         || "ResponseBuilder IdP"
         | 
| 21 | 
            +
                    attributes      = options[:attributes]     || {}
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    # Mandatory for responses
         | 
| 24 | 
            +
                    destination     = options.fetch(:destination)
         | 
| 25 | 
            +
                    in_response_to  = options.fetch(:in_response_to)
         | 
| 26 | 
            +
                    name_id         = options.fetch(:name_id)
         | 
| 27 | 
            +
                    not_on_or_after = options.fetch(:not_on_or_after)
         | 
| 28 | 
            +
                    not_before      = options.fetch(:not_before)
         | 
| 29 | 
            +
                    audience        = options.fetch(:audience)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    # Signature settings
         | 
| 32 | 
            +
                    sign_assertion  = [ true, false ].member?(options[:sign_assertion]) ? options[:sign_assertion] : true
         | 
| 33 | 
            +
                    sign_response   = [ true, false ].member?(options[:sign_response]) ? options[:sign_response] : true
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    # Fixture controls
         | 
| 36 | 
            +
                    skip_assertion  = options[:skip_assertion]
         | 
| 37 | 
            +
                    skip_conditions = options[:skip_conditions]
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
         | 
| 40 | 
            +
                      xml.Response("xmlns:samlp" => NS_MAP["samlp"], "ID" => response_id, "InResponseTo" => in_response_to, "Version" => version, "IssueInstant" => issue_instant, "Destination" => destination) do
         | 
| 41 | 
            +
                        xml.doc.root.add_namespace_definition("saml", NS_MAP["saml"])
         | 
| 42 | 
            +
                        xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "samlp" }
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                        xml["saml"].Issuer(issuer)
         | 
| 45 | 
            +
                        xml["samlp"].Status { |xml| xml["samlp"].StatusCode("Value" => status_code) }
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                        unless skip_assertion
         | 
| 48 | 
            +
                          xml["saml"].Assertion("xmlns:saml" => NS_MAP["saml"], "ID" => assertion_id, "IssueInstant" => issue_instant, "Version" => "2.0") do
         | 
| 49 | 
            +
                            xml["saml"].Issuer(issuer)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                            xml["saml"].Subject do
         | 
| 52 | 
            +
                              xml["saml"].NameID(name_id, "Format" => name_id_format)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                              xml["saml"].SubjectConfirmation("Method" => subject_conf_m) do
         | 
| 55 | 
            +
                                xml["saml"].SubjectConfirmationData("InResponseTo" => in_response_to, "NotOnOrAfter" => not_on_or_after, "Recipient" => destination)
         | 
| 56 | 
            +
                              end
         | 
| 57 | 
            +
                            end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                            unless skip_conditions
         | 
| 60 | 
            +
                              xml["saml"].Conditions("NotBefore" => not_before, "NotOnOrAfter" => not_on_or_after) do
         | 
| 61 | 
            +
                                xml["saml"].AudienceRestriction do
         | 
| 62 | 
            +
                                  xml["saml"].Audience(audience)
         | 
| 63 | 
            +
                                end
         | 
| 64 | 
            +
                              end
         | 
| 65 | 
            +
                            end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                            xml["saml"].AuthnStatement("AuthnInstant" => issue_instant, "SessionIndex" => assertion_id) do
         | 
| 68 | 
            +
                              xml["saml"].AuthnContext do
         | 
| 69 | 
            +
                                xml["saml"].AuthnContextClassRef(auth_context)
         | 
| 70 | 
            +
                              end
         | 
| 71 | 
            +
                            end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                            unless attributes.empty?
         | 
| 74 | 
            +
                              xml["saml"].AttributeStatement do
         | 
| 75 | 
            +
                                attributes.keys.sort.each do |name|
         | 
| 76 | 
            +
                                  xml["saml"].Attribute("Name" => name) do
         | 
| 77 | 
            +
                                    values = Array(attributes[name])
         | 
| 78 | 
            +
                                    values.each do |value|
         | 
| 79 | 
            +
                                      xml["saml"].AttributeValue(value, "xmlns:xsi" => NS_MAP["xsi"], "xmlns:xs" => NS_MAP["xs"], "xsi:type" => "xs:string")
         | 
| 80 | 
            +
                                    end
         | 
| 81 | 
            +
                                  end
         | 
| 82 | 
            +
                                end
         | 
| 83 | 
            +
                              end
         | 
| 84 | 
            +
                            end
         | 
| 85 | 
            +
                          end
         | 
| 86 | 
            +
                        end
         | 
| 87 | 
            +
                      end
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                    # The core response is ready, not on to signing
         | 
| 91 | 
            +
                    response = builder.doc
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                    response = sign(response, assertion_id, options) if sign_assertion
         | 
| 94 | 
            +
                    response = sign(response, response_id, options)  if sign_response
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                    response.to_xml(COMPACT)
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  def self.sign(document, element_id, options)
         | 
| 100 | 
            +
                    certificate  = options[:certificate] || Samlr::Tools::CertificateBuilder.new
         | 
| 101 | 
            +
                    element      = document.at("//*[@ID='#{element_id}']")
         | 
| 102 | 
            +
                    digest       = digest(document, element, options)
         | 
| 103 | 
            +
                    canoned      = digest.at("./ds:SignedInfo", NS_MAP).canonicalize(C14N)
         | 
| 104 | 
            +
                    signature    = certificate.sign(canoned)
         | 
| 105 | 
            +
                    skip_keyinfo = options[:skip_keyinfo]
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    Nokogiri::XML::Builder.with(digest) do |xml|
         | 
| 108 | 
            +
                      xml.SignatureValue(signature)
         | 
| 109 | 
            +
                      xml.KeyInfo do
         | 
| 110 | 
            +
                        xml.X509Data do
         | 
| 111 | 
            +
                          xml.X509Certificate(certificate.x509_as_pem)
         | 
| 112 | 
            +
                        end
         | 
| 113 | 
            +
                      end unless skip_keyinfo
         | 
| 114 | 
            +
                    end
         | 
| 115 | 
            +
                    # digest.root.last_element_child.after "<SignatureValue>#{signature}</SignatureValue>"
         | 
| 116 | 
            +
                    element.at("./saml:Issuer", NS_MAP).add_next_sibling(digest)
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                    document
         | 
| 119 | 
            +
                  end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  def self.digest(document, element, options)
         | 
| 122 | 
            +
                    c14n_method   = options[:c14n_method]   || "http://www.w3.org/2001/10/xml-exc-c14n#"
         | 
| 123 | 
            +
                    sign_method   = options[:sign_method]   || "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
         | 
| 124 | 
            +
                    digest_method = options[:digest_method] || "http://www.w3.org/2000/09/xmldsig#sha1"
         | 
| 125 | 
            +
                    env_signature = options[:env_signature] || "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
         | 
| 126 | 
            +
                    namespaces    = options[:namespaces]    || [ "#default", "samlp", "saml", "ds", "xs", "xsi" ]
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                    canoned       = element.canonicalize(C14N, namespaces)
         | 
| 129 | 
            +
                    digest_value  = Base64.encode64(OpenSSL::Digest::SHA1.new.digest(canoned)).delete("\n")
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                    builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
         | 
| 132 | 
            +
                      xml.Signature("xmlns" => NS_MAP["ds"]) do
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                        xml.SignedInfo do
         | 
| 135 | 
            +
                          xml.CanonicalizationMethod("Algorithm" => c14n_method)
         | 
| 136 | 
            +
                          xml.SignatureMethod("Algorithm" => sign_method)
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                          xml.Reference("URI" => "##{element['ID']}") do
         | 
| 139 | 
            +
                            xml.Transforms do
         | 
| 140 | 
            +
                              xml.Transform("Algorithm" => env_signature)
         | 
| 141 | 
            +
                              xml.Transform("Algorithm" => c14n_method) do
         | 
| 142 | 
            +
                                xml.InclusiveNamespaces("xmlns" => c14n_method, "PrefixList" => namespaces.join(" "))
         | 
| 143 | 
            +
                              end
         | 
| 144 | 
            +
                            end
         | 
| 145 | 
            +
                            xml.DigestMethod("Algorithm" => digest_method)
         | 
| 146 | 
            +
                            xml.DigestValue(digest_value)
         | 
| 147 | 
            +
                          end
         | 
| 148 | 
            +
                        end
         | 
| 149 | 
            +
                      end
         | 
| 150 | 
            +
                    end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                    builder.doc.root
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                end
         | 
| 156 | 
            +
              end
         | 
| 157 | 
            +
            end
         |