ruby-saml-mod 0.1.8 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
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: []