ruby-saml-mod 0.1.27 → 0.1.28

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
  SHA1:
3
- metadata.gz: 9fb25d67d2b17dd051b1b4ced52bbbfc8fac8c3f
4
- data.tar.gz: e1576c19390be8d294128d1a5d3ae60a6d7813d2
3
+ metadata.gz: 364210a5dc8d2c29558b9b0a19c258f64cdb1c9b
4
+ data.tar.gz: 5c389976863d3d599ea7fdcf1c91932f0418b169
5
5
  SHA512:
6
- metadata.gz: 96dadc807bb4f4b26c356f991452ee26b2b54251352d996d65f47256621c79a07a1a4c0690505b704d368311bc8ba2951500b66dd65b1472118738671ea103eb
7
- data.tar.gz: e83f4fa8df332d3301898811e00a8952d6b91099b6a9d1d54439ff639c4e300ac5fb518d9c073b22145c93b4e8ab43e8fed8052dc20791e66cb164b68bae7df2
6
+ metadata.gz: b025fe482372d414d6fc263f5bbc2623e7070daa3d5fe8e2f6004b9e51494260f0746339d71791e7911c10be75b5f692069f2a98dc3d8e99985bf99c39cdab63
7
+ data.tar.gz: 5826d046b3eccb253f38611afde71d6e602778b1e998d3baf3c7ba1629505821b60bfc4687f8f9e5e63164a305b429adfd39b6ccbd2358450636161eb6a73cdd
@@ -2,7 +2,7 @@ module Onelogin::Saml
2
2
  class Response
3
3
 
4
4
  attr_accessor :settings
5
- attr_reader :document, :decrypted_document, :xml, :response
5
+ attr_reader :document, :xml, :response
6
6
  attr_reader :name_id, :name_qualifier, :session_index, :saml_attributes
7
7
  attr_reader :status_code, :status_message
8
8
  attr_reader :in_response_to, :destination, :issuer
@@ -14,14 +14,14 @@ module Onelogin::Saml
14
14
  @xml = Base64.decode64(@response)
15
15
  @document = LibXML::XML::Document.string(@xml)
16
16
  @document.extend(XMLSecurity::SignedDocument)
17
- rescue => e
17
+ rescue
18
18
  # could not parse document, everything is invalid
19
19
  @response = nil
20
20
  return
21
21
  end
22
22
 
23
- @issuer = @document.find_first("/samlp:Response/saml:Issuer", Onelogin::NAMESPACES).content rescue nil
24
- @status_code = @document.find_first("/samlp:Response/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES)["Value"] rescue nil
23
+ @issuer = document.find_first("/samlp:Response/saml:Issuer", Onelogin::NAMESPACES).content rescue nil
24
+ @status_code = document.find_first("/samlp:Response/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES)["Value"] rescue nil
25
25
 
26
26
  process(settings) if settings
27
27
  end
@@ -30,79 +30,119 @@ module Onelogin::Saml
30
30
  @settings = settings
31
31
  return unless @response
32
32
 
33
- @decrypted_document = LibXML::XML::Document.document(@document)
34
- @decrypted_document.extend(XMLSecurity::SignedDocument)
35
- @decrypted_document.decrypt!(@settings)
33
+ @in_response_to = untrusted_find_first("/samlp:Response")['InResponseTo'] rescue nil
34
+ @destination = untrusted_find_first("/samlp:Response")['Destination'] rescue nil
35
+ @status_message = untrusted_find_first("/samlp:Response/samlp:Status/samlp:StatusCode").content rescue nil
36
+
37
+ @name_id = trusted_find_first("saml:Assertion/saml:Subject/saml:NameID").content rescue nil
38
+ @name_qualifier = trusted_find_first("saml:Assertion/saml:Subject/saml:NameID")["NameQualifier"] rescue nil
39
+ @session_index = trusted_find_first("saml:Assertion/saml:AuthnStatement")["SessionIndex"] rescue nil
36
40
 
37
- @in_response_to = @decrypted_document.find_first("/samlp:Response", Onelogin::NAMESPACES)['InResponseTo'] rescue nil
38
- @destination = @decrypted_document.find_first("/samlp:Response", Onelogin::NAMESPACES)['Destination'] rescue nil
39
- @name_id = @decrypted_document.find_first("/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", Onelogin::NAMESPACES).content rescue nil
40
41
  @saml_attributes = {}
41
- @decrypted_document.find("//saml:Attribute", Onelogin::NAMESPACES).each do |attr|
42
+ trusted_find("saml:Attribute").each do |attr|
42
43
  attrname = attr['FriendlyName'] || Onelogin::ATTRIBUTES[attr['Name']] || attr['Name']
43
44
  @saml_attributes[attrname] = attr.content.strip rescue nil
44
45
  end
45
- @name_qualifier = @decrypted_document.find_first("/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", Onelogin::NAMESPACES)["NameQualifier"] rescue nil
46
- @session_index = @decrypted_document.find_first("/samlp:Response/saml:Assertion/saml:AuthnStatement", Onelogin::NAMESPACES)["SessionIndex"] rescue nil
47
- @status_message = @decrypted_document.find_first("/samlp:Response/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).content rescue nil
46
+ end
47
+
48
+ def disable_signature_validation!(settings)
49
+ @settings = settings
50
+ @is_valid = true
51
+ @trusted_roots = [decrypted_document.root]
52
+ end
53
+
54
+ def decrypted_document
55
+ @decrypted_document ||= LibXML::XML::Document.document(document).tap do |doc|
56
+ doc.extend(XMLSecurity::SignedDocument)
57
+ doc.decrypt!(settings)
58
+ end
59
+ end
60
+
61
+ def untrusted_find_first(xpath)
62
+ decrypted_document.find(xpath, Onelogin::NAMESPACES).first
63
+ end
64
+
65
+ def trusted_find_first(xpath)
66
+ trusted_find(xpath).first
67
+ end
68
+
69
+ def trusted_find(xpath)
70
+ trusted_roots.map do |trusted_root|
71
+ trusted_root.find("descendant-or-self::#{xpath}", Onelogin::NAMESPACES).to_a
72
+ end.flatten.compact
48
73
  end
49
74
 
50
75
  def logger=(val)
51
76
  @logger = val
52
77
  end
53
-
78
+
54
79
  def is_valid?
55
- if @response.nil? || @response == ""
80
+ @is_valid ||= validate
81
+ end
82
+
83
+ def validate
84
+ if response.nil? || response == ""
56
85
  @validation_error = "No response to validate"
57
86
  return false
58
87
  end
59
-
60
- if !@settings.idp_cert_fingerprint
88
+
89
+ if !settings.idp_cert_fingerprint
61
90
  @validation_error = "No fingerprint configured in SAML settings"
62
91
  return false
63
92
  end
64
-
93
+
65
94
  # Verify the original document if it has a signature, otherwise verify the signature
66
95
  # in the encrypted portion. If there is no signature, then we can't verify.
67
96
  verified = false
68
- if @document.find_first("//ds:Signature", Onelogin::NAMESPACES)
69
- verified = @document.validate(@settings.idp_cert_fingerprint, @logger)
97
+
98
+ if document.has_signature?
99
+ verified = document.validate(settings.idp_cert_fingerprint, @logger)
70
100
  if !verified
71
- @validation_error = @document.validation_error
101
+ @validation_error = document.validation_error
72
102
  return false
73
103
  end
74
104
  end
75
105
 
76
- if !verified && @decrypted_document.find_first("//ds:Signature", Onelogin::NAMESPACES)
77
- verified = @decrypted_document.validate(@settings.idp_cert_fingerprint, @logger)
106
+ if !verified && decrypted_document.has_signature?
107
+ verified = decrypted_document.validate(settings.idp_cert_fingerprint, @logger)
78
108
  if !verified
79
- @validation_error = @document.validation_error
109
+ @validation_error = decrypted_document.validation_error
80
110
  return false
81
111
  end
82
112
  end
83
-
113
+
84
114
  if !verified
85
115
  @validation_error = "No signature found in the response"
86
116
  return false
87
117
  end
88
-
118
+
119
+ # If we get here, validation has succeeded, and we can trust all
120
+ # <ds:Signature> elements. Each of those has a <ds:Reference> which
121
+ # points to the root of the root of the NodeSet it signs.
122
+ @trusted_roots = decrypted_document.signed_roots
123
+
89
124
  true
90
125
  end
91
-
126
+
127
+ # triggers validation
128
+ def trusted_roots
129
+ is_valid? ? @trusted_roots : []
130
+ end
131
+
92
132
  def success_status?
93
133
  @status_code == Onelogin::Saml::StatusCodes::SUCCESS_URI
94
134
  end
95
-
135
+
96
136
  def auth_failure?
97
137
  @status_code == Onelogin::Saml::StatusCodes::AUTHN_FAILED_URI
98
138
  end
99
-
139
+
100
140
  def no_authn_context?
101
141
  @status_code == Onelogin::Saml::StatusCodes::NO_AUTHN_CONTEXT_URI
102
142
  end
103
-
143
+
104
144
  def fingerprint_from_idp
105
- if base64_cert = @decrypted_document.find_first("//ds:X509Certificate", Onelogin::NAMESPACES)
145
+ if base64_cert = decrypted_document.find_first("//ds:X509Certificate", Onelogin::NAMESPACES)
106
146
  cert_text = Base64.decode64(base64_cert.content)
107
147
  cert = OpenSSL::X509::Certificate.new(cert_text)
108
148
  Digest::SHA1.hexdigest(cert.to_der)
@@ -79,6 +79,9 @@ module XMLSecurity
79
79
  :xmlSecDSigStatusInvalid
80
80
  ]
81
81
 
82
+ XMLSEC_ERRORS_R_INVALID_DATA = 12
83
+ class XmlSecError < ::RuntimeError; end
84
+
82
85
  class XmlSecPtrList < FFI::Struct
83
86
  layout \
84
87
  :id, :string,
@@ -170,6 +173,12 @@ module XMLSecurity
170
173
  :reserved1, :pointer
171
174
  end
172
175
 
176
+ ErrorCallback = FFI::Function.new(:void,
177
+ [ :string, :int, :string, :string, :string, :int, :string ]
178
+ ) do |file, line, func, errorObject, errorSubject, reason, msg |
179
+ XMLSecurity.handle_xmlsec_error_callback(file, line, func, errorObject, errorSubject, reason, msg)
180
+ end
181
+
173
182
  # xmlsec functions
174
183
  attach_function :xmlSecInit, [], :int
175
184
  attach_function :xmlSecParseMemory, [ :pointer, :uint, :int ], :pointer
@@ -193,7 +202,9 @@ module XMLSecurity
193
202
  attach_function :xmlSecEncCtxDecrypt, [ :pointer, :pointer ], :int
194
203
  attach_function :xmlSecEncCtxDestroy, [ :pointer ], :void
195
204
 
205
+ attach_function :xmlSecErrorsDefaultCallback, [ :string, :int, :string, :string, :string, :int, :string ], :void
196
206
  attach_function :xmlSecErrorsDefaultCallbackEnableOutput, [ :bool ], :void
207
+ attach_function :xmlSecErrorsSetCallback, [:pointer], :void
197
208
 
198
209
  attach_function :xmlSecTransformExclC14NGetKlass, [], :pointer
199
210
  attach_function :xmlSecOpenSSLTransformRsaSha1GetKlass, [], :pointer
@@ -238,6 +249,18 @@ module XMLSecurity
238
249
  raise "Failed initializing XMLSec" if self.xmlSecInit < 0
239
250
  raise "Failed initializing app crypto" if self.xmlSecOpenSSLAppInit(nil) < 0
240
251
  raise "Failed initializing crypto" if self.xmlSecOpenSSLInit < 0
252
+ self.xmlSecErrorsSetCallback(ErrorCallback)
253
+
254
+ def self.handle_xmlsec_error_callback(*args)
255
+ raise_exception_if_necessary(*args)
256
+ xmlSecErrorsDefaultCallback(*args)
257
+ end
258
+
259
+ def self.raise_exception_if_necessary(file, line, func, errorObject, errorSubject, reason, msg)
260
+ if reason == XMLSEC_ERRORS_R_INVALID_DATA
261
+ raise XmlSecError.new(msg)
262
+ end
263
+ end
241
264
 
242
265
 
243
266
  def self.mute(&block)
@@ -267,6 +290,29 @@ module XMLSecurity
267
290
  "-----BEGIN PUBLIC KEY-----\n#{base64}-----END PUBLIC KEY-----"
268
291
  end
269
292
 
293
+ def has_signature?
294
+ signatures.any?
295
+ end
296
+
297
+ def signed_roots
298
+ signatures.map do |sig|
299
+ ref = sig.find('.//ds:Reference', Onelogin::NAMESPACES).first
300
+ signed_element_id = ref['URI'].sub(/^#/, '')
301
+
302
+ if signed_element_id.empty?
303
+ self.root
304
+ else
305
+ xpath_id_query = %Q(ancestor::*[@ID = "#{signed_element_id}"])
306
+
307
+ ref.find(xpath_id_query, Onelogin::NAMESPACES).first
308
+ end
309
+ end.compact
310
+ end
311
+
312
+ def signatures
313
+ @signatures ||= self.find("//ds:Signature", Onelogin::NAMESPACES)
314
+ end
315
+
270
316
  def validate(idp_cert_fingerprint, logger = nil)
271
317
  # get cert from response
272
318
  base64_cert = self.find_first("//ds:X509Certificate", Onelogin::NAMESPACES).content
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = %q{ruby-saml-mod}
3
- s.version = "0.1.27"
3
+ s.version = "0.1.28"
4
4
 
5
5
  s.authors = ["OneLogin LLC", "Bracken", "Zach", "Cody", "Jeremy", "Paul"]
6
6
  s.date = %q{2014-05-05}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-saml-mod
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.27
4
+ version: 0.1.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - OneLogin LLC
@@ -89,4 +89,3 @@ signing_key:
89
89
  specification_version: 4
90
90
  summary: Ruby library for SAML service providers
91
91
  test_files: []
92
- has_rdoc: