samlr 2.7.1 → 2.7.2.pre.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 981235b1e8b9c47db48aa1ad9dd69103f68766c4e9f247980e2482f5c0130f38
4
- data.tar.gz: ffda22321c9f2cb8564681747e426f59dc9cd77aed0761bb71e88d5c43ac393d
3
+ metadata.gz: f29fb87e4f9c2e892ff008b93ceaa0406683aefb33b5b7772933498db217324d
4
+ data.tar.gz: 87e0905875456dac62a5bb66a4436b6549ff10a1859d42d54dcd426acb7026db
5
5
  SHA512:
6
- metadata.gz: 3001de515250f4deb3fff83454aab102c7c72a712304206a88a398b4a5db4f0aa00fc618ae9c85964b8ccfd60cefbc7b13b2fccb50820851b8257e22943a64af
7
- data.tar.gz: df04ff380856705c2b15ce7bdbde71708c12d5d8ae3c518debfcd6f7042987fd7b347433bb09eb7f69f5391ac5f30feb3589756e51e34fa3b0da30d068b62700
6
+ metadata.gz: a540ead119f309d5bba8a85641fe379a15ca421a48f1be082327f99c2c3bc30e94b7b26db756e2a1490170937e9518ea9fd5aa917e316b15b79657fa79102b6c
7
+ data.tar.gz: 2bad133e71423fa9207488c470832244e20bf889a25befa6e5718ffca5176c46ecb0129521091dcd8dfa0421d33314d65db2ca330b91f463826284cbcc4eaf0f
@@ -5,7 +5,7 @@ module Samlr
5
5
 
6
6
  def initialize(document, options)
7
7
  @document = document
8
- @options = options
8
+ @options = options
9
9
  end
10
10
 
11
11
  def verify!
@@ -31,15 +31,15 @@ module Samlr
31
31
  def attributes
32
32
  @attributes ||= {}.tap do |attrs|
33
33
  assertion.xpath("./saml:AttributeStatement/saml:Attribute", NS_MAP).each do |statement|
34
- name = statement["Name"]
34
+ name = statement["Name"]
35
35
  values = statement.xpath("./saml:AttributeValue", NS_MAP)
36
36
 
37
- if values.size == 0
38
- value = nil
37
+ value = if values.size == 0
38
+ nil
39
39
  elsif values.size == 1
40
- value = values.first.text
40
+ values.first.text
41
41
  else
42
- value = values.map { |value| value.text }
42
+ values.map { |value| value.text }
43
43
  end
44
44
 
45
45
  attrs[name] = value
@@ -56,7 +56,7 @@ module Samlr
56
56
  end
57
57
 
58
58
  def name_id_options
59
- @name_id_options ||= Hash[name_id_node.attributes.map{|k,v| [k, v.value]}]
59
+ @name_id_options ||= name_id_node.attributes.map { |k, v| [k, v.value] }.to_h
60
60
  end
61
61
 
62
62
  def conditions
data/lib/samlr/command.rb CHANGED
@@ -4,10 +4,10 @@ require "logger"
4
4
  module Samlr
5
5
  # Helper module for command line options
6
6
  module Command
7
- COMMANDS = [ :verify, :schema_validate, :print ]
7
+ COMMANDS = [:verify, :schema_validate, :print]
8
8
 
9
9
  def self.execute(options, path = nil)
10
- Samlr.logger.level = Logger::DEBUG if options[:verbose]
10
+ Samlr.logger.level = Logger::DEBUG if options[:verbose]
11
11
  Samlr.validation_mode = :log if options[:skip_validation]
12
12
 
13
13
  if options[:verify]
@@ -21,21 +21,18 @@ module Samlr
21
21
  execute_verify(path, options)
22
22
  end
23
23
  elsif options[:schema_validate]
24
- Samlr::Tools.validate(:path => path)
24
+ Samlr::Tools.validate(path: path)
25
25
  elsif options[:print]
26
26
  Samlr::Response.parse(File.read(path)).to_xml
27
27
  end
28
28
  end
29
29
 
30
- private
31
-
32
30
  def self.execute_verify(path, options)
33
- begin
34
- Samlr::Response.new(File.read(path), options).verify!
35
- "Verification passed for #{path}"
36
- rescue Samlr::SamlrError => e
37
- "Verification failed for #{path}: #{e.message}"
38
- end
31
+ Samlr::Response.new(File.read(path), options).verify!
32
+ "Verification passed for #{path}"
33
+ rescue Samlr::SamlrError => e
34
+ "Verification failed for #{path}: #{e.message}"
39
35
  end
36
+ private_class_method :execute_verify
40
37
  end
41
38
  end
@@ -3,10 +3,10 @@ module Samlr
3
3
  attr_reader :audience, :not_before, :not_on_or_after, :options
4
4
 
5
5
  def initialize(condition, options)
6
- @options = options
7
- @not_before = (condition || {})["NotBefore"]
6
+ @options = options
7
+ @not_before = (condition || {})["NotBefore"]
8
8
  @not_on_or_after = (condition || {})["NotOnOrAfter"]
9
- @audience = extract_audience(condition)
9
+ @audience = extract_audience(condition)
10
10
  end
11
11
 
12
12
  def verify!
@@ -35,9 +35,9 @@ module Samlr
35
35
 
36
36
  def audience_satisfied?
37
37
  options[:audience].nil? ||
38
- audience.nil? ||
39
- audience.empty? ||
40
- audience.any? { |a| options[:audience] === a }
38
+ audience.nil? ||
39
+ audience.empty? ||
40
+ audience.any? { |a| options[:audience] === a }
41
41
  end
42
42
 
43
43
  private
@@ -45,10 +45,10 @@ module Samlr
45
45
  def extract_audience(condition)
46
46
  return unless condition
47
47
 
48
- audience_restriction_node = condition.at('./saml:AudienceRestriction', NS_MAP)
48
+ audience_restriction_node = condition.at("./saml:AudienceRestriction", NS_MAP)
49
49
  return unless audience_restriction_node
50
50
 
51
- audience_nodes = audience_restriction_node.search('./saml:Audience', NS_MAP)
51
+ audience_nodes = audience_restriction_node.search("./saml:Audience", NS_MAP)
52
52
  return unless audience_nodes.any?
53
53
 
54
54
  audience_nodes.map(&:text)
@@ -3,16 +3,16 @@ module Samlr
3
3
  attr_accessor :value
4
4
 
5
5
  def initialize(value)
6
- if value.is_a?(OpenSSL::X509::Certificate)
7
- @value = self.class.x509(value)
6
+ @value = if value.is_a?(OpenSSL::X509::Certificate)
7
+ self.class.x509(value)
8
8
  else
9
- @value = self.class.normalize(value)
9
+ self.class.normalize(value)
10
10
  end
11
11
  end
12
12
 
13
13
  def self.from_string(string)
14
14
  normalized = normalize(string)
15
- if string.gsub(':', '').length == 64
15
+ if string.delete(":").length == 64
16
16
  FingerprintSHA256.new(normalized)
17
17
  else
18
18
  FingerprintSHA1.new(normalized)
@@ -46,7 +46,7 @@ module Samlr
46
46
 
47
47
  # Extracts a fingerprint for an x509 certificate
48
48
  def self.x509(certificate)
49
- raise NotImplementedError, 'subclass must implement x509'
49
+ raise NotImplementedError, "subclass must implement x509"
50
50
  end
51
51
 
52
52
  # Converts a string to fingerprint normal form
@@ -4,7 +4,7 @@ module Samlr
4
4
  class FingerprintSHA1 < Fingerprint
5
5
  # Extracts a fingerprint for an x509 certificate
6
6
  def self.x509(certificate)
7
- normalize(OpenSSL::Digest::SHA1.new.hexdigest(certificate.to_der))
7
+ normalize(OpenSSL::Digest.new("SHA1").hexdigest(certificate.to_der))
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Samlr
4
4
  class FingerprintSHA256 < Fingerprint
5
5
  # Extracts a fingerprint for an x509 certificate
6
6
  def self.x509(certificate)
7
- normalize(OpenSSL::Digest::SHA256.new.hexdigest(certificate.to_der))
7
+ normalize(OpenSSL::Digest.new("SHA256").hexdigest(certificate.to_der))
8
8
  end
9
9
  end
10
10
  end
@@ -6,7 +6,7 @@ module Samlr
6
6
 
7
7
  def initialize(node)
8
8
  @node = node
9
- @uri = node["URI"][1..-1]
9
+ @uri = node["URI"][1..]
10
10
  end
11
11
 
12
12
  def digest_method
@@ -14,7 +14,7 @@ module Samlr
14
14
  end
15
15
 
16
16
  def digest_value
17
- @digest_value ||= node.at("./ds:DigestValue", NS_MAP).text
17
+ @digest_value ||= node.at("./ds:DigestValue", NS_MAP).text
18
18
  end
19
19
 
20
20
  def decoded_digest_value
@@ -27,6 +27,5 @@ module Samlr
27
27
  attribute ? attribute.split(" ") : []
28
28
  end
29
29
  end
30
-
31
30
  end
32
31
  end
data/lib/samlr/request.rb CHANGED
@@ -41,14 +41,14 @@ module Samlr
41
41
  Samlr::Tools.parse(data, compressed: true)
42
42
  end
43
43
 
44
- def get_attribute_or_element(x_path,attribute=nil)
44
+ def get_attribute_or_element(x_path, attribute = nil)
45
45
  if document
46
46
  element = document.xpath(x_path)
47
47
  if element.length == 0
48
48
  nil
49
49
  elsif attribute
50
50
  value = element.attr(attribute)
51
- value.to_s if value
51
+ value&.to_s
52
52
  else
53
53
  element
54
54
  end
@@ -2,7 +2,6 @@ require "forwardable"
2
2
  require "nokogiri"
3
3
 
4
4
  module Samlr
5
-
6
5
  # This is the object interface to the XML response object.
7
6
  class Response
8
7
  extend Forwardable
@@ -11,7 +10,7 @@ module Samlr
11
10
  attr_reader :document, :options
12
11
 
13
12
  def initialize(data, options)
14
- @options = options
13
+ @options = options
15
14
  @document = Response.parse(data)
16
15
  end
17
16
 
@@ -13,11 +13,11 @@ module Samlr
13
13
  # Signature validations require document alterations
14
14
  @original = original
15
15
  @document = original.dup
16
- @prefix = prefix
17
- @options = options
16
+ @prefix = prefix
17
+ @options = options
18
18
  @signature = nil
19
19
 
20
- id = @document.at("#{prefix}", NS_MAP)&.attribute('ID')
20
+ id = @document.at(prefix.to_s, NS_MAP)&.attribute("ID")
21
21
  @signature = find_signature_for_element_id(id) if id
22
22
 
23
23
  @fingerprint = if options[:fingerprint]
@@ -51,7 +51,6 @@ module Samlr
51
51
  refs_xpath.each do |ref|
52
52
  refs << Samlr::Reference.new(ref)
53
53
  end
54
-
55
54
  end
56
55
  end
57
56
 
@@ -70,7 +69,7 @@ module Samlr
70
69
  def verify_digests!
71
70
  # Check if we need to remove an enveloped signature
72
71
  if @signature && !@signature_removed
73
- signed_element = @document.at("#{prefix}", NS_MAP)
72
+ signed_element = @document.at(prefix.to_s, NS_MAP)
74
73
  is_enveloped = signed_element&.xpath(".//ds:Signature", NS_MAP)&.include?(@signature)
75
74
 
76
75
  # Remove enveloped signature for digest verification
@@ -81,9 +80,9 @@ module Samlr
81
80
  end
82
81
 
83
82
  references.each do |reference|
84
- node = referenced_node(reference.uri)
83
+ node = referenced_node(reference.uri)
85
84
  canoned = node.canonicalize(C14N, reference.namespaces)
86
- digest = reference.digest_method.digest(canoned)
85
+ digest = reference.digest_method.digest(canoned)
87
86
 
88
87
  if digest != reference.decoded_digest_value
89
88
  raise SignatureError.new("Reference validation error: Digest mismatch for #{reference.uri}")
@@ -128,14 +127,10 @@ module Samlr
128
127
  end
129
128
 
130
129
  def certificate
131
- @certificate ||= begin
132
- if node = certificate_node
133
- Certificate.new(Base64.decode64(node.text))
134
- elsif cert = options[:certificate]
135
- Certificate.new(cert)
136
- else
137
- nil
138
- end
130
+ @certificate ||= if (node = certificate_node)
131
+ Certificate.new(Base64.decode64(node.text))
132
+ elsif (cert = options[:certificate])
133
+ Certificate.new(cert)
139
134
  end
140
135
  end
141
136
 
@@ -150,9 +145,14 @@ module Samlr
150
145
  def find_signature_for_element_id(element_id)
151
146
  return nil unless element_id
152
147
 
153
- return @document.at_xpath("//ds:Signature[ds:SignedInfo/ds:Reference[@URI='##{element_id}']]", NS_MAP)
154
-
148
+ # Prevent XPath injection by using parameterized XPath queries with variable bindings.
149
+ # Nokogiri's xpath() method supports passing variables separately from the query,
150
+ # which prevents injection attacks like "_x'or'1'='1" from breaking out of the predicate.
151
+ @document.at_xpath(
152
+ "//ds:Signature[ds:SignedInfo/ds:Reference[@URI=$uri]]",
153
+ NS_MAP,
154
+ {uri: "##{element_id}"}
155
+ )
155
156
  end
156
-
157
157
  end
158
158
  end
@@ -1,35 +1,34 @@
1
1
  module Samlr
2
2
  module Tools
3
-
4
3
  # Container for generating/referencing X509 and keys
5
4
  class CertificateBuilder
6
5
  attr_reader :key_size
7
6
 
8
7
  def initialize(options = {})
9
8
  @key_size = options.fetch(:key_size, 4096)
10
- @x509 = options[:x509]
9
+ @x509 = options[:x509]
11
10
  @key_pair = options[:key_pair]
12
11
  end
13
12
 
14
13
  def x509
15
14
  @x509 ||= begin
16
15
  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
- ])
16
+ name = OpenSSL::X509::Name.new([
17
+ ["C", "US", OpenSSL::ASN1::PRINTABLESTRING],
18
+ ["O", domain, OpenSSL::ASN1::UTF8STRING],
19
+ ["OU", "Samlr ResponseBuilder", OpenSSL::ASN1::UTF8STRING],
20
+ ["CN", "CA"]
21
+ ])
23
22
 
24
23
  certificate = OpenSSL::X509::Certificate.new
25
- certificate.subject = name
26
- certificate.issuer = name
24
+ certificate.subject = name
25
+ certificate.issuer = name
27
26
  certificate.not_before = (Time.now - 5)
28
- certificate.not_after = (Time.now + 60 * 60 * 24 * 365 * 20)
27
+ certificate.not_after = (Time.now + 60 * 60 * 24 * 365 * 20)
29
28
  certificate.public_key = key_pair.public_key
30
- certificate.serial = 1
31
- certificate.version = 2
32
- certificate.sign(key_pair, OpenSSL::Digest::SHA1.new)
29
+ certificate.serial = 1
30
+ certificate.version = 2
31
+ certificate.sign(key_pair, OpenSSL::Digest.new("SHA1"))
33
32
 
34
33
  certificate
35
34
  end
@@ -47,11 +46,11 @@ module Samlr
47
46
  end
48
47
 
49
48
  def sign(string)
50
- Base64.encode64(key_pair.sign(OpenSSL::Digest::SHA1.new, string)).delete("\n")
49
+ Base64.encode64(key_pair.sign(OpenSSL::Digest.new("SHA1"), string)).delete("\n")
51
50
  end
52
51
 
53
52
  def verify(signature, string)
54
- key_pair.public_key.verify(OpenSSL::Digest::SHA1.new, Base64.decode64(signature), string)
53
+ key_pair.public_key.verify(OpenSSL::Digest.new("SHA1"), Base64.decode64(signature), string)
55
54
  end
56
55
 
57
56
  def to_certificate
@@ -59,15 +58,15 @@ module Samlr
59
58
  end
60
59
 
61
60
  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) }
61
+ File.write(File.join(path, "#{id}_private_key.pem"), certificate.key_pair.to_pem)
62
+ File.write(File.join(path, "#{id}_certificate.pem"), certificate.x509.to_pem)
64
63
  end
65
64
 
66
65
  def self.load(path, id = "samlr")
67
- key_pair = OpenSSL::PKey::RSA.new(File.read(File.join(path, "#{id}_private_key.pem")))
66
+ key_pair = OpenSSL::PKey::RSA.new(File.read(File.join(path, "#{id}_private_key.pem")))
68
67
  x509_cert = OpenSSL::X509::Certificate.new(File.read(File.join(path, "#{id}_certificate.pem")))
69
68
 
70
- new(:key_pair => key_pair, :x509 => x509_cert)
69
+ new(key_pair: key_pair, x509: x509_cert)
71
70
  end
72
71
  end
73
72
  end
@@ -7,7 +7,7 @@ module Samlr
7
7
  def self.build(options = {})
8
8
  # Mandatory
9
9
  name_id = options.fetch(:name_id)
10
- issuer = options.fetch(:issuer)
10
+ issuer = options.fetch(:issuer)
11
11
 
12
12
  builder = Nokogiri::XML::Builder.new do |xml|
13
13
  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
@@ -21,10 +21,10 @@ module Samlr
21
21
  end
22
22
 
23
23
  def self.logout_options(options)
24
- name_id_options = options[:name_id_options] || {}
25
- options = { "Format" => format_option(options) }
26
- options.merge!("NameQualifier" => name_id_options[:name_qualifier]) if name_id_options[:name_qualifier]
27
- options.merge!("SPNameQualifier" => name_id_options[:spname_qualifier]) if name_id_options[:spname_qualifier]
24
+ name_id_options = options[:name_id_options] || {}
25
+ options = {"Format" => format_option(options)}
26
+ options["NameQualifier"] = name_id_options[:name_qualifier] if name_id_options[:name_qualifier]
27
+ options["SPNameQualifier"] = name_id_options[:spname_qualifier] if name_id_options[:spname_qualifier]
28
28
  options
29
29
  end
30
30
 
@@ -25,7 +25,7 @@ module Samlr
25
25
  "Version" => "2.0"
26
26
  }
27
27
  result["InResponseTo"] = options[:in_response_to] if options[:in_response_to]
28
- result["Destination"] = options[:destination] if options[:destination]
28
+ result["Destination"] = options[:destination] if options[:destination]
29
29
  result
30
30
  end
31
31
  end
@@ -1,6 +1,5 @@
1
1
  module Samlr
2
2
  module Tools
3
-
4
3
  # Builds you some SP metadata. Accepts a hash with the below keys. No support for arrays
5
4
  # of name id formats or asserion consumer services, build it if you need it.
6
5
  #
@@ -8,16 +7,15 @@ module Samlr
8
7
  # :name_identity_format => Samlr::EMAIL_FORMAT,
9
8
  # :consumer_service_url => "https://sp.example.org/saml"
10
9
  class MetadataBuilder
11
-
12
10
  def self.build(options = {})
13
- name_identity_format = options[:name_identity_format]
14
- consumer_service_url = options[:consumer_service_url]
11
+ name_identity_format = options[:name_identity_format]
12
+ consumer_service_url = options[:consumer_service_url]
15
13
  consumer_service_binding = options[:consumer_service_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
16
- metadata_id = options[:metadata_id] || Samlr::Tools.uuid
17
- sign_metadata = options[:sign_metadata] || false
14
+ metadata_id = options[:metadata_id] || Samlr::Tools.uuid
15
+ sign_metadata = options[:sign_metadata] || false
18
16
 
19
17
  # Mandatory
20
- entity_id = options.fetch(:entity_id)
18
+ entity_id = options.fetch(:entity_id)
21
19
 
22
20
  builder = Nokogiri::XML::Builder.new do |xml|
23
21
  xml.EntityDescriptor("xmlns:md" => NS_MAP["md"], "ID" => metadata_id, "entityID" => entity_id) do
@@ -2,15 +2,14 @@ require "nokogiri"
2
2
 
3
3
  module Samlr
4
4
  module Tools
5
-
6
5
  # Use this for building the SAML auth request XML
7
6
  module RequestBuilder
8
7
  def self.build(options = {})
9
8
  consumer_service_url = options[:consumer_service_url]
10
- issuer = options[:issuer]
9
+ issuer = options[:issuer]
11
10
  name_identity_format = options[:name_identity_format]
12
- allow_create = options[:allow_create] || "true"
13
- authn_context = options[:authn_context]
11
+ allow_create = options[:allow_create] || "true"
12
+ authn_context = options[:authn_context]
14
13
 
15
14
  builder = Nokogiri::XML::Builder.new do |xml|
16
15
  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
@@ -38,7 +37,6 @@ module Samlr
38
37
 
39
38
  builder.to_xml(COMPACT)
40
39
  end
41
-
42
40
  end
43
41
  end
44
42
  end
@@ -4,41 +4,39 @@ require "uuidtools"
4
4
 
5
5
  module Samlr
6
6
  module Tools
7
-
8
7
  # Use this for building test data, not ready to use for production data
9
8
  module ResponseBuilder
10
-
11
9
  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
- name_id = options[:name_id]
23
- name_qualifier = options[:name_qualifier]
10
+ issue_instant = options[:issue_instant] || Samlr::Tools::Timestamp.stamp
11
+ response_id = options[:response_id] || Samlr::Tools.uuid
12
+ assertion_id = options[:assertion_id] || Samlr::Tools.uuid
13
+ status_code = options[:status_code] || "urn:oasis:names:tc:SAML:2.0:status:Success"
14
+ name_id_format = options[:name_id_format] || EMAIL_FORMAT
15
+ subject_conf_m = options[:subject_conf_m] || "urn:oasis:names:tc:SAML:2.0:cm:bearer"
16
+ version = options[:version] || "2.0"
17
+ auth_context = options[:auth_context] || "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
18
+ issuer = options[:issuer] || "ResponseBuilder IdP"
19
+ attributes = options[:attributes] || {}
20
+ name_id = options[:name_id]
21
+ name_qualifier = options[:name_qualifier]
24
22
  sp_name_qualifier = options[:sp_name_qualifier]
25
23
 
26
24
  # Mandatory for responses
27
- destination = options.fetch(:destination)
28
- in_response_to = options.fetch(:in_response_to)
25
+ destination = options.fetch(:destination)
26
+ in_response_to = options.fetch(:in_response_to)
29
27
  not_on_or_after = options.fetch(:not_on_or_after)
30
- not_before = options.fetch(:not_before)
31
- audience = options.fetch(:audience)
28
+ not_before = options.fetch(:not_before)
29
+ audience = options.fetch(:audience)
32
30
 
33
31
  # Signature settings
34
- sign_assertion = [ true, false ].member?(options[:sign_assertion]) ? options[:sign_assertion] : true
35
- sign_response = [ true, false ].member?(options[:sign_response]) ? options[:sign_response] : true
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
36
34
 
37
35
  # Fixture controls
38
- skip_assertion = options[:skip_assertion]
36
+ skip_assertion = options[:skip_assertion]
39
37
  skip_conditions = options[:skip_conditions]
40
38
 
41
- builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
39
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
42
40
  xml.Response("xmlns:samlp" => NS_MAP["samlp"], "ID" => response_id, "InResponseTo" => in_response_to, "Version" => version, "IssueInstant" => issue_instant, "Destination" => destination) do
43
41
  xml.doc.root.add_namespace_definition("saml", NS_MAP["saml"])
44
42
  xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "samlp" }
@@ -51,9 +49,9 @@ module Samlr
51
49
  xml["saml"].Issuer(issuer)
52
50
 
53
51
  xml["saml"].Subject do
54
- name_id_options = { "Format" => name_id_format}
55
- name_id_options.merge!("NameQualifier" => name_qualifier) unless name_qualifier.nil?
56
- name_id_options.merge!("SPNameQualifier" => sp_name_qualifier) unless sp_name_qualifier.nil?
52
+ name_id_options = {"Format" => name_id_format}
53
+ name_id_options["NameQualifier"] = name_qualifier unless name_qualifier.nil?
54
+ name_id_options["SPNameQualifier"] = sp_name_qualifier unless sp_name_qualifier.nil?
57
55
 
58
56
  xml["saml"].NameID(name_id, name_id_options) unless name_id.nil?
59
57
 
@@ -99,30 +97,32 @@ module Samlr
99
97
 
100
98
  # The core response is ready, not on to signing
101
99
  response = builder.doc
102
- assertion_options = options.merge(:skip_keyinfo => options[:skip_assertion_keyinfo])
100
+ assertion_options = options.merge(skip_keyinfo: options[:skip_assertion_keyinfo])
103
101
  response = sign(response, assertion_id, assertion_options) if sign_assertion
104
102
 
105
- response_options = options.merge(:skip_keyinfo => options[:skip_response_keyinfo])
106
- response = sign(response, response_id, response_options) if sign_response
103
+ response_options = options.merge(skip_keyinfo: options[:skip_response_keyinfo])
104
+ response = sign(response, response_id, response_options) if sign_response
107
105
 
108
106
  response.to_xml(COMPACT)
109
107
  end
110
108
 
111
109
  def self.sign(document, element_id, options)
112
- certificate = options[:certificate] || Samlr::Tools::CertificateBuilder.new
113
- element = document.at("//*[@ID='#{element_id}']")
114
- digest = digest(document, element, options)
115
- canoned = digest.at("./ds:SignedInfo", NS_MAP).canonicalize(C14N)
116
- signature = certificate.sign(canoned)
110
+ certificate = options[:certificate] || Samlr::Tools::CertificateBuilder.new
111
+ element = document.at("//*[@ID='#{element_id}']")
112
+ digest = digest(document, element, options)
113
+ canoned = digest.at("./ds:SignedInfo", NS_MAP).canonicalize(C14N)
114
+ signature = certificate.sign(canoned)
117
115
  skip_keyinfo = options[:skip_keyinfo]
118
116
 
119
117
  Nokogiri::XML::Builder.with(digest) do |xml|
120
118
  xml.SignatureValue(signature)
121
- xml.KeyInfo do
122
- xml.X509Data do
123
- xml.X509Certificate(certificate.x509_as_pem)
119
+ unless skip_keyinfo
120
+ xml.KeyInfo do
121
+ xml.X509Data do
122
+ xml.X509Certificate(certificate.x509_as_pem)
123
+ end
124
124
  end
125
- end unless skip_keyinfo
125
+ end
126
126
  end
127
127
  # digest.root.last_element_child.after "<SignatureValue>#{signature}</SignatureValue>"
128
128
  if element.at("./saml:Issuer", NS_MAP)
@@ -135,23 +135,22 @@ module Samlr
135
135
  end
136
136
 
137
137
  def self.digest(document, element, options)
138
- c14n_method = options[:c14n_method] || "http://www.w3.org/2001/10/xml-exc-c14n#"
139
- sign_method = options[:sign_method] || "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
138
+ c14n_method = options[:c14n_method] || "http://www.w3.org/2001/10/xml-exc-c14n#"
139
+ sign_method = options[:sign_method] || "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
140
140
  digest_method = options[:digest_method] || "http://www.w3.org/2000/09/xmldsig#sha1"
141
141
  env_signature = options[:env_signature] || "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
142
- namespaces = options[:namespaces] || [ "#default", "samlp", "saml", "ds", "xs", "xsi" ]
142
+ namespaces = options[:namespaces] || ["#default", "samlp", "saml", "ds", "xs", "xsi"]
143
143
 
144
- canoned = element.canonicalize(C14N, namespaces)
145
- digest_value = Base64.encode64(OpenSSL::Digest::SHA1.new.digest(canoned)).delete("\n")
144
+ canoned = element.canonicalize(C14N, namespaces)
145
+ digest_value = Base64.encode64(OpenSSL::Digest.new("SHA1").digest(canoned)).delete("\n")
146
146
 
147
- builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
147
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
148
148
  xml.Signature("xmlns" => NS_MAP["ds"]) do
149
-
150
149
  xml.SignedInfo do
151
150
  xml.CanonicalizationMethod("Algorithm" => c14n_method)
152
151
  xml.SignatureMethod("Algorithm" => sign_method)
153
152
 
154
- xml.Reference("URI" => "##{element['ID']}") do
153
+ xml.Reference("URI" => "##{element["ID"]}") do
155
154
  xml.Transforms do
156
155
  xml.Transform("Algorithm" => env_signature)
157
156
  xml.Transform("Algorithm" => c14n_method) do
@@ -167,7 +166,6 @@ module Samlr
167
166
 
168
167
  builder.doc.root
169
168
  end
170
-
171
169
  end
172
170
  end
173
171
  end
@@ -1,7 +1,6 @@
1
1
  module Samlr
2
2
  module Tools
3
3
  module Timestamp
4
-
5
4
  # Generate a current timestamp in ISO8601 format
6
5
  def self.stamp(time = Time.now)
7
6
  time.utc.iso8601
@@ -20,7 +19,6 @@ module Samlr
20
19
  def self.not_before?(time)
21
20
  Time.now.to_i >= (time.to_i - Samlr.jitter.to_i)
22
21
  end
23
-
24
22
  end
25
23
  end
26
24
  end
data/lib/samlr/tools.rb CHANGED
@@ -15,10 +15,10 @@ require "samlr/tools/logout_response_builder"
15
15
  module Samlr
16
16
  module Tools
17
17
  SHA_MAP = {
18
- 1 => OpenSSL::Digest::SHA1,
19
- 256 => OpenSSL::Digest::SHA256,
20
- 384 => OpenSSL::Digest::SHA384,
21
- 512 => OpenSSL::Digest::SHA512
18
+ 1 => OpenSSL::Digest::SHA1,
19
+ 256 => OpenSSL::Digest::SHA256,
20
+ 384 => OpenSSL::Digest::SHA384,
21
+ 512 => OpenSSL::Digest::SHA512
22
22
  }
23
23
 
24
24
  # Convert algorithm attribute value to Ruby implementation
@@ -32,13 +32,13 @@ module Samlr
32
32
 
33
33
  # Accepts a document and optionally :path => xpath, :c14n_mode => c14n_mode
34
34
  def self.canonicalize(xml, options = {})
35
- options = { :c14n_mode => C14N }.merge(options)
35
+ options = {c14n_mode: C14N}.merge(options)
36
36
  document = Nokogiri::XML(xml) { |c| c.strict.noblanks }
37
37
 
38
- if path = options[:path]
39
- node = document.at(path, NS_MAP)
38
+ node = if (path = options[:path])
39
+ document.at(path, NS_MAP)
40
40
  else
41
- node = document
41
+ document
42
42
  end
43
43
 
44
44
  node.canonicalize(options[:c14n_mode], options[:namespaces])
@@ -52,17 +52,16 @@ module Samlr
52
52
  # Deflates, Base64 encodes and CGI escapes a string
53
53
  def self.encode(string)
54
54
  deflated = Zlib::Deflate.deflate(string, 9)[2..-5]
55
- encoded = Base64.encode64(deflated)
56
- escaped = CGI.escape(encoded)
57
- escaped
55
+ encoded = Base64.encode64(deflated)
56
+ CGI.escape(encoded)
58
57
  end
59
58
 
60
59
  # CGI unescapes, Base64 decodes and inflates a string
61
60
  def self.decode(string)
62
61
  unescaped = CGI.unescape(string)
63
- decoded = Base64.decode64(unescaped)
64
- inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
65
- inflated = inflater.inflate(decoded)
62
+ decoded = Base64.decode64(unescaped)
63
+ inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
64
+ inflated = inflater.inflate(decoded)
66
65
 
67
66
  inflater.finish
68
67
  inflater.close
@@ -71,29 +70,29 @@ module Samlr
71
70
  end
72
71
 
73
72
  def self.validate!(options = {})
74
- validate(options.merge(:bang => true))
73
+ validate(options.merge(bang: true))
75
74
  end
76
75
 
77
76
  # Validate a SAML request or response against an XSD. Supply either :path or :document in the options and
78
77
  # a :schema (defaults to SAML validation)
79
78
  def self.validate(options = {})
80
79
  document = options[:document] || File.read(options[:path])
81
- schema = options.fetch(:schema, SAML_SCHEMA)
82
- bang = options.fetch(:bang, false)
80
+ schema = options.fetch(:schema, SAML_SCHEMA)
81
+ bang = options.fetch(:bang, false)
83
82
 
84
- if document.is_a?(Nokogiri::XML::Document)
85
- xml = document
83
+ xml = if document.is_a?(Nokogiri::XML::Document)
84
+ document
86
85
  else
87
- xml = Nokogiri::XML(document) { |c| c.strict }
86
+ Nokogiri::XML(document) { |c| c.strict }
88
87
  end
89
88
 
90
89
  # All bundled schemas are using relative schemaLocation. This means we'll have to
91
90
  # change working directory to find them during validation.
92
91
  Dir.chdir(Samlr.schema_location) do
93
- if schema.is_a?(Nokogiri::XML::Schema)
94
- xsd = schema
92
+ xsd = if schema.is_a?(Nokogiri::XML::Schema)
93
+ schema
95
94
  else
96
- xsd = Nokogiri::XML::Schema(File.read(schema))
95
+ Nokogiri::XML::Schema(File.read(schema))
97
96
  end
98
97
 
99
98
  result = xsd.validate(xml)
@@ -113,7 +112,7 @@ module Samlr
113
112
  def self.parse(data, compressed: false)
114
113
  return unless data
115
114
  decoded = Base64.decode64(data)
116
- decoded = self.inflate(decoded) if compressed
115
+ decoded = inflate(decoded) if compressed
117
116
  return unless decoded
118
117
  begin
119
118
  doc = Nokogiri::XML(decoded) { |config| config.strict }
@@ -126,7 +125,7 @@ module Samlr
126
125
  end
127
126
 
128
127
  begin
129
- Samlr::Tools.validate!(:document => doc)
128
+ Samlr::Tools.validate!(document: doc)
130
129
  rescue Samlr::SamlrError => e
131
130
  Samlr.logger.warn("Accepting non schema conforming response: #{e.message}, #{e.details}")
132
131
  raise e unless Samlr.validation_mode == :log
@@ -135,7 +134,7 @@ module Samlr
135
134
  end
136
135
 
137
136
  def self.inflate(data)
138
- inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
137
+ inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
139
138
  decoded = inflater.inflate(data)
140
139
  inflater.finish
141
140
  inflater.close
data/lib/samlr/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Samlr
2
- VERSION = "2.7.1"
2
+ VERSION = "2.7.2.pre.1"
3
3
  end
data/lib/samlr.rb CHANGED
@@ -2,22 +2,22 @@ require "nokogiri"
2
2
  require "logger"
3
3
 
4
4
  module Samlr
5
- C14N = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
6
- COMPACT = { :indent => 0, :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML }
5
+ C14N = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
6
+ COMPACT = {indent: 0, save_with: Nokogiri::XML::Node::SaveOptions::AS_XML}
7
7
 
8
- NS_MAP = {
9
- "c14n" => "http://www.w3.org/2001/10/xml-exc-c14n#",
10
- "ds" => "http://www.w3.org/2000/09/xmldsig#",
11
- "saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
8
+ NS_MAP = {
9
+ "c14n" => "http://www.w3.org/2001/10/xml-exc-c14n#",
10
+ "ds" => "http://www.w3.org/2000/09/xmldsig#",
11
+ "saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
12
12
  "samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
13
- "md" => "urn:oasis:names:tc:SAML:2.0:metadata",
14
- "xsi" => "http://www.w3.org/2001/XMLSchema-instance",
15
- "xs" => "http://www.w3.org/2001/XMLSchema"
13
+ "md" => "urn:oasis:names:tc:SAML:2.0:metadata",
14
+ "xsi" => "http://www.w3.org/2001/XMLSchema-instance",
15
+ "xs" => "http://www.w3.org/2001/XMLSchema"
16
16
  }
17
17
 
18
18
  EMAIL_FORMAT = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
19
- SAML_SCHEMA = "saml-schema-protocol-2.0.xsd"
20
- META_SCHEMA = "saml-schema-metadata-2.0.xsd"
19
+ SAML_SCHEMA = "saml-schema-protocol-2.0.xsd"
20
+ META_SCHEMA = "saml-schema-metadata-2.0.xsd"
21
21
 
22
22
  class << self
23
23
  attr_accessor :schema_location
@@ -28,9 +28,9 @@ module Samlr
28
28
 
29
29
  self.schema_location = File.join(File.dirname(__FILE__), "..", "config", "schemas")
30
30
  self.validation_mode = :reject
31
- self.jitter = 0
32
- self.logger = Logger.new(STDERR)
33
- self.logger.level = Logger::UNKNOWN
31
+ self.jitter = 0
32
+ self.logger = Logger.new($stderr)
33
+ logger.level = Logger::UNKNOWN
34
34
  end
35
35
 
36
36
  unless Object.new.respond_to?(:try)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samlr
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.1
4
+ version: 2.7.2.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Morten Primdahl