C0 code coverage information
Generated on Fri Oct 31 16:37:32 -0700 2008 with rcov 0.7.0
Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
1 require "openid/dh"
2 require "openid/util"
3 require "openid/kvpost"
4 require "openid/cryptutil"
5 require "openid/protocolerror"
6 require "openid/association"
7
8 module OpenID
9 class Consumer
10
11 # A superclass for implementing Diffie-Hellman association sessions.
12 class DiffieHellmanSession
13 class << self
14 attr_reader :session_type, :secret_size, :allowed_assoc_types,
15 :hashfunc
16 end
17
18 def initialize(dh=nil)
19 if dh.nil?
20 dh = DiffieHellman.from_defaults
21 end
22 @dh = dh
23 end
24
25 # Return the query parameters for requesting an association
26 # using this Diffie-Hellman association session
27 def get_request
28 args = {'dh_consumer_public' => CryptUtil.num_to_base64(@dh.public)}
29 if (!@dh.using_default_values?)
30 args['dh_modulus'] = CryptUtil.num_to_base64(@dh.modulus)
31 args['dh_gen'] = CryptUtil.num_to_base64(@dh.generator)
32 end
33
34 return args
35 end
36
37 # Process the response from a successful association request and
38 # return the shared secret for this association
39 def extract_secret(response)
40 dh_server_public64 = response.get_arg(OPENID_NS, 'dh_server_public',
41 NO_DEFAULT)
42 enc_mac_key64 = response.get_arg(OPENID_NS, 'enc_mac_key', NO_DEFAULT)
43 dh_server_public = CryptUtil.base64_to_num(dh_server_public64)
44 enc_mac_key = Util.from_base64(enc_mac_key64)
45 return @dh.xor_secret(self.class.hashfunc,
46 dh_server_public, enc_mac_key)
47 end
48 end
49
50 # A Diffie-Hellman association session that uses SHA1 as its hash
51 # function
52 class DiffieHellmanSHA1Session < DiffieHellmanSession
53 @session_type = 'DH-SHA1'
54 @secret_size = 20
55 @allowed_assoc_types = ['HMAC-SHA1']
56 @hashfunc = CryptUtil.method(:sha1)
57 end
58
59 # A Diffie-Hellman association session that uses SHA256 as its hash
60 # function
61 class DiffieHellmanSHA256Session < DiffieHellmanSession
62 @session_type = 'DH-SHA256'
63 @secret_size = 32
64 @allowed_assoc_types = ['HMAC-SHA256']
65 @hashfunc = CryptUtil.method(:sha256)
66 end
67
68 # An association session that does not use encryption
69 class NoEncryptionSession
70 class << self
71 attr_reader :session_type, :allowed_assoc_types
72 end
73 @session_type = 'no-encryption'
74 @allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
75
76 def get_request
77 return {}
78 end
79
80 def extract_secret(response)
81 mac_key64 = response.get_arg(OPENID_NS, 'mac_key', NO_DEFAULT)
82 return Util.from_base64(mac_key64)
83 end
84 end
85
86 # An object that manages creating and storing associations for an
87 # OpenID provider endpoint
88 class AssociationManager
89 def self.create_session(session_type)
90 case session_type
91 when 'no-encryption'
92 NoEncryptionSession.new
93 when 'DH-SHA1'
94 DiffieHellmanSHA1Session.new
95 when 'DH-SHA256'
96 DiffieHellmanSHA256Session.new
97 else
98 raise ArgumentError, "Unknown association session type: "\
99 "#{session_type.inspect}"
100 end
101 end
102
103 def initialize(store, server_url, compatibility_mode=false,
104 negotiator=nil)
105 @store = store
106 @server_url = server_url
107 @compatibility_mode = compatibility_mode
108 @negotiator = negotiator || DefaultNegotiator
109 end
110
111 def get_association
112 if @store.nil?
113 return nil
114 end
115
116 assoc = @store.get_association(@server_url)
117 if assoc.nil? || assoc.expires_in <= 0
118 assoc = negotiate_association
119 if !assoc.nil?
120 @store.store_association(@server_url, assoc)
121 end
122 end
123
124 return assoc
125 end
126
127 def negotiate_association
128 assoc_type, session_type = @negotiator.get_allowed_type
129 begin
130 return request_association(assoc_type, session_type)
131 rescue ServerError => why
132 supported_types = extract_supported_association_type(why, assoc_type)
133 if !supported_types.nil?
134 # Attempt to create an association from the assoc_type and
135 # session_type that the server told us it supported.
136 assoc_type, session_type = supported_types
137 begin
138 return request_association(assoc_type, session_type)
139 rescue ServerError => why
140 Util.log("Server #{@server_url} refused its suggested " \
141 "association type: session_type=#{session_type}, " \
142 "assoc_type=#{assoc_type}")
143 return nil
144 end
145 end
146 end
147 end
148
149 protected
150 def extract_supported_association_type(server_error, assoc_type)
151 # Any error message whose code is not 'unsupported-type' should
152 # be considered a total failure.
153 if (server_error.error_code != 'unsupported-type' or
154 server_error.message.is_openid1)
155 Util.log("Server error when requesting an association from "\
156 "#{@server_url}: #{server_error.error_text}")
157 return nil
158 end
159
160 # The server didn't like the association/session type that we
161 # sent, and it sent us back a message that might tell us how to
162 # handle it.
163 Util.log("Unsupported association type #{assoc_type}: "\
164 "#{server_error.error_text}")
165
166 # Extract the session_type and assoc_type from the error message
167 assoc_type = server_error.message.get_arg(OPENID_NS, 'assoc_type')
168 session_type = server_error.message.get_arg(OPENID_NS, 'session_type')
169
170 if assoc_type.nil? or session_type.nil?
171 Util.log("Server #{@server_url} responded with unsupported "\
172 "association session but did not supply a fallback.")
173 return nil
174 elsif !@negotiator.allowed?(assoc_type, session_type)
175 Util.log("Server sent unsupported session/association type: "\
176 "session_type=#{session_type}, assoc_type=#{assoc_type}")
177 return nil
178 else
179 return [assoc_type, session_type]
180 end
181 end
182
183 # Make and process one association request to this endpoint's OP
184 # endpoint URL. Returns an association object or nil if the
185 # association processing failed. Raises ServerError when the
186 # remote OpenID server returns an error.
187 def request_association(assoc_type, session_type)
188 assoc_session, args = create_associate_request(assoc_type, session_type)
189
190 begin
191 response = OpenID.make_kv_post(args, @server_url)
192 return extract_association(response, assoc_session)
193 rescue HTTPStatusError => why
194 Util.log("Got HTTP status error when requesting association: #{why}")
195 return nil
196 rescue Message::KeyNotFound => why
197 Util.log("Missing required parameter in response from "\
198 "#{@server_url}: #{why}")
199 return nil
200
201 rescue ProtocolError => why
202 Util.log("Protocol error processing response from #{@server_url}: "\
203 "#{why}")
204 return nil
205 end
206 end
207
208 # Create an association request for the given assoc_type and
209 # session_type. Returns a pair of the association session object
210 # and the request message that will be sent to the server.
211 def create_associate_request(assoc_type, session_type)
212 assoc_session = self.class.create_session(session_type)
213 args = {
214 'mode' => 'associate',
215 'assoc_type' => assoc_type,
216 }
217
218 if !@compatibility_mode
219 args['ns'] = OPENID2_NS
220 end
221
222 # Leave out the session type if we're in compatibility mode
223 # *and* it's no-encryption.
224 if !@compatibility_mode ||
225 assoc_session.class.session_type != 'no-encryption'
226 args['session_type'] = assoc_session.class.session_type
227 end
228
229 args.merge!(assoc_session.get_request)
230 message = Message.from_openid_args(args)
231 return assoc_session, message
232 end
233
234 # Given an association response message, extract the OpenID 1.X
235 # session type. Returns the association type for this message
236 #
237 # This function mostly takes care of the 'no-encryption' default
238 # behavior in OpenID 1.
239 #
240 # If the association type is plain-text, this function will
241 # return 'no-encryption'
242 def get_openid1_session_type(assoc_response)
243 # If it's an OpenID 1 message, allow session_type to default
244 # to nil (which signifies "no-encryption")
245 session_type = assoc_response.get_arg(OPENID1_NS, 'session_type')
246
247 # Handle the differences between no-encryption association
248 # respones in OpenID 1 and 2:
249
250 # no-encryption is not really a valid session type for
251 # OpenID 1, but we'll accept it anyway, while issuing a
252 # warning.
253 if session_type == 'no-encryption'
254 Util.log("WARNING: #{@server_url} sent 'no-encryption'"\
255 "for OpenID 1.X")
256
257 # Missing or empty session type is the way to flag a
258 # 'no-encryption' response. Change the session type to
259 # 'no-encryption' so that it can be handled in the same
260 # way as OpenID 2 'no-encryption' respones.
261 elsif session_type == '' || session_type.nil?
262 session_type = 'no-encryption'
263 end
264
265 return session_type
266 end
267
268 def self.extract_expires_in(message)
269 # expires_in should be a base-10 string.
270 expires_in_str = message.get_arg(OPENID_NS, 'expires_in', NO_DEFAULT)
271 if !(/\A\d+\Z/ =~ expires_in_str)
272 raise ProtocolError, "Invalid expires_in field: #{expires_in_str}"
273 end
274 expires_in_str.to_i
275 end
276
277 # Attempt to extract an association from the response, given the
278 # association response message and the established association
279 # session.
280 def extract_association(assoc_response, assoc_session)
281 # Extract the common fields from the response, raising an
282 # exception if they are not found
283 assoc_type = assoc_response.get_arg(OPENID_NS, 'assoc_type',
284 NO_DEFAULT)
285 assoc_handle = assoc_response.get_arg(OPENID_NS, 'assoc_handle',
286 NO_DEFAULT)
287 expires_in = self.class.extract_expires_in(assoc_response)
288
289 # OpenID 1 has funny association session behaviour.
290 if assoc_response.is_openid1
291 session_type = get_openid1_session_type(assoc_response)
292 else
293 session_type = assoc_response.get_arg(OPENID2_NS, 'session_type',
294 NO_DEFAULT)
295 end
296
297 # Session type mismatch
298 if assoc_session.class.session_type != session_type
299 if (assoc_response.is_openid1 and session_type == 'no-encryption')
300 # In OpenID 1, any association request can result in a
301 # 'no-encryption' association response. Setting
302 # assoc_session to a new no-encryption session should
303 # make the rest of this function work properly for
304 # that case.
305 assoc_session = NoEncryptionSession.new
306 else
307 # Any other mismatch, regardless of protocol version
308 # results in the failure of the association session
309 # altogether.
310 raise ProtocolError, "Session type mismatch. Expected "\
311 "#{assoc_session.class.session_type}, got "\
312 "#{session_type}"
313 end
314 end
315
316 # Make sure assoc_type is valid for session_type
317 if !assoc_session.class.allowed_assoc_types.member?(assoc_type)
318 raise ProtocolError, "Unsupported assoc_type for session "\
319 "#{assoc_session.class.session_type} "\
320 "returned: #{assoc_type}"
321 end
322
323 # Delegate to the association session to extract the secret
324 # from the response, however is appropriate for that session
325 # type.
326 begin
327 secret = assoc_session.extract_secret(assoc_response)
328 rescue Message::KeyNotFound, ArgumentError => why
329 raise ProtocolError, "Malformed response for "\
330 "#{assoc_session.class.session_type} "\
331 "session: #{why.message}"
332 end
333
334
335 return Association.from_expires_in(expires_in, assoc_handle, secret,
336 assoc_type)
337 end
338 end
339 end
340 end
Generated using the rcov code coverage analysis tool for Ruby version 0.7.0.