ruby-saml-mod 0.1.8 → 0.1.9

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.
data/lib/onelogin/saml.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'zlib'
2
2
  require "base64"
3
- require "rexml/document"
3
+ require "xml/libxml"
4
4
  require "xml_sec"
5
5
 
6
6
  module Onelogin
@@ -12,11 +12,11 @@ module Onelogin::Saml
12
12
  zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
13
13
  @xml = zlib.inflate(@xml)
14
14
  @document = XMLSecurity::SignedDocument.new(@xml)
15
-
16
- @in_response_to = REXML::XPath.first(@document, "/samlp:LogoutResponse", Onelogin::NAMESPACES).attributes['InResponseTo'] rescue nil
17
- @destination = REXML::XPath.first(@document, "/samlp:LogoutResponse", Onelogin::NAMESPACES).attributes['Destination'] rescue nil
18
- @status_code = REXML::XPath.first(@document, "/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).attributes["Value"] rescue nil
19
- @status_message = REXML::XPath.first(@document, "/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).text rescue nil
15
+
16
+ @in_response_to = @document.find_first("/samlp:LogoutResponse", Onelogin::NAMESPACES)['InResponseTo'] rescue nil
17
+ @destination = @document.find_first("/samlp:LogoutResponse", Onelogin::NAMESPACES)['Destination'] rescue nil
18
+ @status_code = @document.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES)['Value'] rescue nil
19
+ @status_message = @document.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).content rescue nil
20
20
  end
21
21
 
22
22
  def logger=(val)
@@ -1,8 +1,8 @@
1
1
  module Onelogin::Saml
2
2
  class Response
3
3
 
4
- attr_reader :settings, :document, :xml, :response
5
- attr_reader :name_id, :name_qualifier, :session_index
4
+ attr_reader :settings, :document, :decrypted_document, :xml, :response
5
+ attr_reader :name_id, :name_qualifier, :session_index, :saml_attributes
6
6
  attr_reader :status_code, :status_message
7
7
  attr_reader :in_response_to, :destination
8
8
  attr_reader :validation_error
@@ -11,16 +11,24 @@ module Onelogin::Saml
11
11
  @settings = settings
12
12
 
13
13
  @xml = Base64.decode64(@response)
14
- @document = XMLSecurity::SignedDocument.new(@xml)
15
- @document.decrypt(@settings)
14
+ @document = LibXML::XML::Document.string(@xml)
15
+ @document.extend(XMLSecurity::SignedDocument)
16
16
 
17
- @in_response_to = REXML::XPath.first(@document, "/samlp:Response", Onelogin::NAMESPACES).attributes['InResponseTo'] rescue nil
18
- @destination = REXML::XPath.first(@document, "/samlp:Response", Onelogin::NAMESPACES).attributes['Destination'] rescue nil
19
- @name_id = REXML::XPath.first(@document, "/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", Onelogin::NAMESPACES).text rescue nil
20
- @name_qualifier = REXML::XPath.first(@document, "/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", Onelogin::NAMESPACES).attributes["NameQualifier"] rescue nil
21
- @session_index = REXML::XPath.first(@document, "/samlp:Response/saml:Assertion/saml:AuthnStatement", Onelogin::NAMESPACES).attributes["SessionIndex"] rescue nil
22
- @status_code = REXML::XPath.first(@document, "/samlp:Response/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).attributes["Value"] rescue nil
23
- @status_message = REXML::XPath.first(@document, "/samlp:Response/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).text rescue nil
17
+ @decrypted_document = LibXML::XML::Document.document(@document)
18
+ @decrypted_document.extend(XMLSecurity::SignedDocument)
19
+ @decrypted_document.decrypt(@settings)
20
+
21
+ @in_response_to = @decrypted_document.find_first("/samlp:Response", Onelogin::NAMESPACES)['InResponseTo'] rescue nil
22
+ @destination = @decrypted_document.find_first("/samlp:Response", Onelogin::NAMESPACES)['Destination'] rescue nil
23
+ @name_id = @decrypted_document.find_first("/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", Onelogin::NAMESPACES).content rescue nil
24
+ @saml_attributes = {}
25
+ @decrypted_document.find("//saml:Attribute", Onelogin::NAMESPACES).each do |attr|
26
+ @saml_attributes[attr['FriendlyName']] = attr.content.strip rescue nil
27
+ end
28
+ @name_qualifier = @decrypted_document.find_first("/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", Onelogin::NAMESPACES)["NameQualifier"] rescue nil
29
+ @session_index = @decrypted_document.find_first("/samlp:Response/saml:Assertion/saml:AuthnStatement", Onelogin::NAMESPACES)["SessionIndex"] rescue nil
30
+ @status_code = @decrypted_document.find_first("/samlp:Response/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES)["Value"] rescue nil
31
+ @status_message = @decrypted_document.find_first("/samlp:Response/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).content rescue nil
24
32
  end
25
33
 
26
34
  def logger=(val)
@@ -28,23 +36,27 @@ module Onelogin::Saml
28
36
  end
29
37
 
30
38
  def is_valid?
31
- if !@response.blank? && @document.elements["//ds:X509Certificate"]
32
- if !@settings.idp_cert_fingerprint
33
- @validation_error = "No fingerprint configured in SAML settings"
34
- false
35
- elsif @document.validate(@settings.idp_cert_fingerprint, @logger)
36
- true
37
- else
38
- @validation_error = @document.validation_error
39
- false
40
- end
41
- elsif @response.blank?
39
+ if @response.nil? || @response == ""
42
40
  @validation_error = "No response to validate"
43
- false
44
- else
41
+ return false
42
+ end
43
+
44
+ if @document.find_first("//ds:X509Certificate", Onelogin::NAMESPACES).nil?
45
45
  @validation_error = "No ds:X509Certificate element"
46
- false
46
+ return false
47
47
  end
48
+
49
+ if !@settings.idp_cert_fingerprint
50
+ @validation_error = "No fingerprint configured in SAML settings"
51
+ return false
52
+ end
53
+
54
+ if !@document.validate(@settings.idp_cert_fingerprint, @logger)
55
+ @validation_error = @document.validation_error
56
+ return false
57
+ end
58
+
59
+ true
48
60
  end
49
61
 
50
62
  def success_status?
@@ -60,8 +72,8 @@ module Onelogin::Saml
60
72
  end
61
73
 
62
74
  def fingerprint_from_idp
63
- if base64_cert = @document.elements["//ds:X509Certificate"]
64
- cert_text = Base64.decode64(base64_cert.text)
75
+ if base64_cert = @document.find_first("//ds:X509Certificate")
76
+ cert_text = Base64.decode64(base64_cert.content)
65
77
  cert = OpenSSL::X509::Certificate.new(cert_text)
66
78
  Digest::SHA1.hexdigest(cert.to_der)
67
79
  else
data/lib/xml_sec.rb CHANGED
@@ -23,93 +23,99 @@
23
23
  # Portions Copyrighted 2007 Todd W Saxton.
24
24
 
25
25
  require 'rubygems'
26
- require "rexml/document"
27
- require "rexml/xpath"
26
+ require "xml/libxml"
28
27
  require "openssl"
29
- require "xmlcanonicalizer"
30
28
  require "digest/sha1"
31
29
  require "tempfile"
32
30
  require "shellwords"
33
31
 
34
32
  module XMLSecurity
35
-
36
- class SignedDocument < REXML::Document
37
-
33
+ module SignedDocument
38
34
  attr_reader :validation_error
39
35
 
40
- def validate (idp_cert_fingerprint, logger = nil)
36
+ def validate(idp_cert_fingerprint, logger = nil)
41
37
  # get cert from response
42
- base64_cert = self.elements["//ds:X509Certificate"].text
43
- cert_text = Base64.decode64(base64_cert)
44
- cert = OpenSSL::X509::Certificate.new(cert_text)
45
-
38
+ base64_cert = self.find_first("//ds:X509Certificate", Onelogin::NAMESPACES).content
39
+ cert_text = Base64.decode64(base64_cert)
40
+ cert = OpenSSL::X509::Certificate.new(cert_text)
41
+
46
42
  # check cert matches registered idp cert
47
- fingerprint = Digest::SHA1.hexdigest(cert.to_der)
48
- valid_flag = fingerprint == idp_cert_fingerprint.gsub(":", "").downcase
49
- @validation_error = "Invalid fingerprint" unless valid_flag
50
-
51
- return valid_flag if !valid_flag
52
-
43
+ fingerprint = Digest::SHA1.hexdigest(cert.to_der)
44
+ expected_fingerprint = idp_cert_fingerprint.gsub(":", "").downcase
45
+ if fingerprint != expected_fingerprint
46
+ @validation_error = "Invalid fingerprint (expected #{expected_fingerprint}, got #{fingerprint})"
47
+ return false
48
+ end
49
+
53
50
  validate_doc(base64_cert, logger)
54
51
  end
55
-
52
+
53
+ def canonicalize_node(node)
54
+ tmp_document = LibXML::XML::Document.new
55
+ tmp_document.root = tmp_document.import(node)
56
+ tmp_document.canonicalize
57
+ end
58
+
56
59
  def validate_doc(base64_cert, logger)
57
60
  # validate references
58
61
 
59
- # remove signature node
60
- sig_element = REXML::XPath.first(self, "//ds:Signature", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"})
61
- sig_element.remove
62
+ sig_element = find_first("//ds:Signature", { "ds" => "http://www.w3.org/2000/09/xmldsig#" })
62
63
 
63
- #check digests
64
- REXML::XPath.each(sig_element, "//ds:Reference", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}) do | ref |
65
-
66
- uri = ref.attributes.get_attribute("URI").value
67
- hashed_element = REXML::XPath.first(self, "//[@ID='#{uri[1,uri.size]}']")
68
- canoner = XML::Util::XmlCanonicalizer.new(false, true)
69
- canon_hashed_element = canoner.canonicalize(hashed_element)
70
- hash = Base64.encode64(Digest::SHA1.digest(canon_hashed_element)).chomp
71
- digest_value = REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}).text
72
-
73
- valid_flag = hash == digest_value
74
-
75
- if !valid_flag
76
- @validation_error = <<-INFO
77
- Invalid references digest.
78
- Got digest of
79
- #{hash}
80
- but expected
81
- #{digest_value}
82
- XML from response:
83
- #{hashed_element}
84
- Canonized XML:
85
- #{canon_hashed_element}
86
- INFO
64
+ # check digests
65
+ sig_element.find("//ds:Reference", { "ds" => "http://www.w3.org/2000/09/xmldsig#" }).each do |ref|
66
+ # Find the referenced element
67
+ uri = ref["URI"]
68
+ ref_element = find_first("//*[@ID='#{uri[1,uri.size]}']")
69
+
70
+ # Create a copy document with it
71
+ ref_document = LibXML::XML::Document.new
72
+ ref_document.root = ref_document.import(ref_element)
73
+
74
+ # Remove the Signature node
75
+ ref_document_sig_element = ref_document.find_first("//ds:Signature", { "ds" => "http://www.w3.org/2000/09/xmldsig#" })
76
+ ref_document_sig_element.remove! if ref_document_sig_element
77
+
78
+ # Canonicalize the referenced element's document
79
+ ref_document_canonicalized = ref_document.canonicalize
80
+ hash = Base64::encode64(Digest::SHA1.digest(ref_document_canonicalized)).chomp
81
+ digest_value = sig_element.find_first("//ds:DigestValue", { "ds" => "http://www.w3.org/2000/09/xmldsig#" }).content
82
+
83
+ if hash != digest_value
84
+ @validation_error = <<-EOF.gsub(/^\s+/, '')
85
+ Invalid references digest.
86
+ Got digest of
87
+ #{hash}
88
+ but expected
89
+ #{digest_value}
90
+ XML from response:
91
+ #{ref_document.to_s(:indent => false)}
92
+ Canonized XML:
93
+ #{ref_document_canonicalized}
94
+ EOF
95
+ return false
87
96
  end
88
-
89
- return valid_flag if !valid_flag
90
97
  end
91
98
 
92
99
  # verify signature
93
- canoner = XML::Util::XmlCanonicalizer.new(false, true)
94
- signed_info_element = REXML::XPath.first(sig_element, "//ds:SignedInfo", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"})
95
- canon_string = canoner.canonicalize(signed_info_element)
100
+ signed_info_element = sig_element.find_first("//ds:SignedInfo", { "ds" => "http://www.w3.org/2000/09/xmldsig#" })
101
+ canon_string = canonicalize_node(signed_info_element)
96
102
 
97
- base64_signature = REXML::XPath.first(sig_element, "//ds:SignatureValue", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}).text
98
- signature = Base64.decode64(base64_signature)
99
-
100
- # get certificate object
101
- cert_text = Base64.decode64(base64_cert)
102
- cert = OpenSSL::X509::Certificate.new(cert_text)
103
-
104
- valid_flag = cert.public_key.verify(OpenSSL::Digest::SHA1.new, signature, canon_string)
105
- @validation_error = "Invalid public key" unless valid_flag
106
-
107
- return valid_flag
103
+ base64_signature = sig_element.find_first("//ds:SignatureValue", { "ds" => "http://www.w3.org/2000/09/xmldsig#" }).content
104
+ signature = Base64.decode64(base64_signature)
105
+
106
+ cert_text = Base64.decode64(base64_cert)
107
+ cert = OpenSSL::X509::Certificate.new(cert_text)
108
+
109
+ if !cert.public_key.verify(OpenSSL::Digest::SHA1.new, signature, canon_string)
110
+ @validation_error = "Invalid public key"
111
+ return false
112
+ end
113
+ return true
108
114
  end
109
115
 
110
116
  def decrypt(settings)
111
117
  if settings.encryption_configured?
112
- REXML::XPath.each(self, "//xenc:EncryptedData", Onelogin::NAMESPACES) do |node|
118
+ find("//xenc:EncryptedData", Onelogin::NAMESPACES).each do |node|
113
119
  Tempfile.open("ruby-saml-decrypt") do |f|
114
120
  f.puts node.to_s
115
121
  f.close
@@ -119,9 +125,11 @@ INFO
119
125
  @logger.warn "Could not decrypt: #{decrypted_xml}" if @logger
120
126
  return false
121
127
  else
122
- decrypted_doc = REXML::Document.new(decrypted_xml)
128
+ decrypted_doc = LibXML::XML::Document.string(decrypted_xml)
123
129
  decrypted_node = decrypted_doc.root
124
- node.parent.replace_with(decrypted_node)
130
+ decrypted_node = self.import(decrypted_node)
131
+ node.parent.next = decrypted_node
132
+ node.parent.remove!
125
133
  end
126
134
  f.unlink
127
135
  end
@@ -1,9 +1,9 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = %q{ruby-saml-mod}
3
- s.version = "0.1.8"
3
+ s.version = "0.1.9"
4
4
 
5
5
  s.authors = ["OneLogin LLC", "Bracken", "Zach", "Cody"]
6
- s.date = %q{2012-02-06}
6
+ s.date = %q{2012-04-27}
7
7
  s.extra_rdoc_files = [
8
8
  "LICENSE"
9
9
  ]
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
23
23
  "lib/xml_sec.rb",
24
24
  "ruby-saml-mod.gemspec"
25
25
  ]
26
+ s.add_dependency('libxml-ruby', '>= 2.3.0')
26
27
  s.homepage = %q{http://github.com/bracken/ruby-saml}
27
28
  s.require_paths = ["lib"]
28
29
  s.summary = %q{Ruby library for SAML service providers}
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-saml-mod
3
3
  version: !ruby/object:Gem::Version
4
- hash: 11
4
+ hash: 9
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 8
10
- version: 0.1.8
9
+ - 9
10
+ version: 0.1.9
11
11
  platform: ruby
12
12
  authors:
13
13
  - OneLogin LLC
@@ -18,9 +18,24 @@ autorequire:
18
18
  bindir: bin
19
19
  cert_chain: []
20
20
 
21
- date: 2012-02-06 00:00:00 Z
22
- dependencies: []
23
-
21
+ date: 2012-04-27 00:00:00 Z
22
+ dependencies:
23
+ - !ruby/object:Gem::Dependency
24
+ name: libxml-ruby
25
+ prerelease: false
26
+ requirement: &id001 !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ hash: 3
32
+ segments:
33
+ - 2
34
+ - 3
35
+ - 0
36
+ version: 2.3.0
37
+ type: :runtime
38
+ version_requirements: *id001
24
39
  description: "This is an early fork from https://github.com/onelogin/ruby-saml - I plan to \"rebase\" these changes ontop of their current version eventually. "
25
40
  email:
26
41
  executables: []