global_session 3.2.10 → 3.3.0

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.
@@ -39,6 +39,9 @@ module GlobalSession::Session
39
39
  # * The sign and verify algorithms, while safe, do not comply fully with PKCS7; they rely on the
40
40
  # OpenSSL low-level crypto API instead of using the higher-level EVP (envelope) API.
41
41
  class V1 < Abstract
42
+ # Pattern that matches strings that are probably a V1 session cookie.
43
+ HEADER = /^eN/
44
+
42
45
  # Utility method to decode a cookie; good for console debugging. This performs no
43
46
  # validation or security check of any sort.
44
47
  #
@@ -233,15 +236,15 @@ module GlobalSession::Session
233
236
  #Check signature
234
237
  expected = canonical_digest(hash)
235
238
  signer = @directory.authorities[authority]
236
- raise SecurityError, "Unknown signing authority #{authority}" unless signer
239
+ raise GlobalSession::InvalidSignature, "Unknown signing authority #{authority}" unless signer
237
240
  got = signer.public_decrypt(GlobalSession::Encoding::Base64Cookie.load(signature))
238
241
  unless (got == expected)
239
- raise SecurityError, "Signature mismatch on global session cookie; tampering suspected"
242
+ raise GlobalSession::InvalidSignature, "Global session integrity failure; tampering suspected"
240
243
  end
241
244
 
242
245
  #Check trust in signing authority
243
246
  unless @directory.trusted_authority?(authority)
244
- raise SecurityError, "Global sessions signed by #{authority} are not trusted"
247
+ raise GlobalSession::InvalidSignature, "Global sessions signed by #{authority} are not trusted"
245
248
  end
246
249
 
247
250
  #Check expiration
@@ -266,8 +269,6 @@ module GlobalSession::Session
266
269
  end
267
270
 
268
271
  def create_from_scratch # :nodoc:
269
- authority_check
270
-
271
272
  @signed = {}
272
273
  @insecure = {}
273
274
  @created_at = Time.now.utc
@@ -275,14 +276,5 @@ module GlobalSession::Session
275
276
  @id = RightSupport::Data::UUID.generate
276
277
  renew!
277
278
  end
278
-
279
- def create_invalid # :nodoc:
280
- @id = nil
281
- @created_at = Time.now.utc
282
- @expired_at = created_at
283
- @signed = {}
284
- @insecure = {}
285
- @authority = nil
286
- end
287
279
  end
288
280
  end
@@ -44,6 +44,9 @@ module GlobalSession::Session
44
44
  # * The sign and verify algorithms, while safe, do not comply fully with PKCS7; they rely on the
45
45
  # OpenSSL low-level crypto API instead of using the higher-level EVP (envelope) API.
46
46
  class V2 < Abstract
47
+ # Pattern that matches strings that are probably a V2 session cookie.
48
+ HEADER = /^l9/
49
+
47
50
  # Utility method to decode a cookie; good for console debugging. This performs no
48
51
  # validation or security check of any sort.
49
52
  #
@@ -79,8 +82,8 @@ module GlobalSession::Session
79
82
  hash['a'] = authority
80
83
  signed_hash = RightSupport::Crypto::SignedHash.new(
81
84
  hash.reject { |k,v| ['dx', 's'].include?(k) },
82
- :encoding=>GlobalSession::Encoding::Msgpack,
83
- :private_key=>@directory.private_key)
85
+ @directory.private_key,
86
+ encoding: GlobalSession::Encoding::Msgpack)
84
87
  @signature = signed_hash.sign(@expired_at)
85
88
  end
86
89
 
@@ -252,20 +255,20 @@ module GlobalSession::Session
252
255
 
253
256
  #Check trust in signing authority
254
257
  unless @directory.trusted_authority?(authority)
255
- raise SecurityError, "Global sessions signed by #{authority.inspect} are not trusted"
258
+ raise GlobalSession::InvalidSignature, "Global sessions signed by #{authority.inspect} are not trusted"
256
259
  end
257
260
 
258
261
  signed_hash = RightSupport::Crypto::SignedHash.new(
259
262
  hash.reject { |k,v| ['dx', 's'].include?(k) },
260
- :encoding=>GlobalSession::Encoding::Msgpack,
261
- :public_key=>@directory.authorities[authority])
263
+ @directory.authorities[authority],
264
+ :encoding=>GlobalSession::Encoding::Msgpack)
262
265
 
263
266
  begin
264
267
  signed_hash.verify!(signature, expired_at)
265
268
  rescue RightSupport::Crypto::ExpiredSignature
266
269
  raise GlobalSession::ExpiredSession, "Session expired at #{expired_at}"
267
270
  rescue RightSupport::Crypto::InvalidSignature => e
268
- raise SecurityError, "Global session signature verification failed: " + e.message
271
+ raise GlobalSession::InvalidSignature, "Integrity check failed: " + e.message
269
272
  end
270
273
 
271
274
  #Check other validity (delegate to directory)
@@ -285,8 +288,6 @@ module GlobalSession::Session
285
288
  end
286
289
 
287
290
  def create_from_scratch # :nodoc:
288
- authority_check
289
-
290
291
  @signed = {}
291
292
  @insecure = {}
292
293
  @created_at = Time.now.utc
@@ -294,14 +295,5 @@ module GlobalSession::Session
294
295
  @id = RightSupport::Data::UUID.generate
295
296
  renew!
296
297
  end
297
-
298
- def create_invalid # :nodoc:
299
- @id = nil
300
- @created_at = Time.now.utc
301
- @expired_at = created_at
302
- @signed = {}
303
- @insecure = {}
304
- @authority = nil
305
- end
306
298
  end
307
299
  end
@@ -22,7 +22,7 @@
22
22
  # Standard library dependencies
23
23
  require 'set'
24
24
 
25
- # Cryptographical Hash
25
+ # SignedHash, which encapsulates the crypto bit of global sessions
26
26
  require 'right_support/crypto'
27
27
 
28
28
  module GlobalSession::Session
@@ -47,6 +47,9 @@ module GlobalSession::Session
47
47
  # encoding (instead of msgpack), and uses the undocumented OpenSSL::PKey#sign and #verify
48
48
  # operations which rely on the PKCS7-compliant OpenSSL EVP API.
49
49
  class V3 < Abstract
50
+ # Pattern that matches strings that are probably a V3 session cookie.
51
+ HEADER = /^WzM/
52
+
50
53
  STRING_ENCODING = !!(RUBY_VERSION !~ /1.8/)
51
54
 
52
55
  # Utility method to decode a cookie; good for console debugging. This performs no
@@ -110,33 +113,6 @@ module GlobalSession::Session
110
113
  result
111
114
  end
112
115
 
113
- # Delete a key from the global session attributes. If the key exists,
114
- # mark the global session dirty
115
- #
116
- # @param [String] the key to delete
117
- # @return [Object] the value of the key deleted, or nil if not found
118
- def delete(key)
119
- key = key.to_s #take care of symbol-style keys
120
- raise GlobalSession::InvalidSession unless valid?
121
-
122
- if @schema_signed.include?(key)
123
- authority_check
124
-
125
- # Only mark dirty if the key actually exists
126
- @dirty_secure = true if @signed.keys.include? key
127
- value = @signed.delete(key)
128
- elsif @schema_insecure.include?(key)
129
-
130
- # Only mark dirty if the key actually exists
131
- @dirty_insecure = true if @insecure.keys.include? key
132
- value = @insecure.delete(key)
133
- else
134
- raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
135
- end
136
-
137
- return value
138
- end
139
-
140
116
  # Serialize the session to a form suitable for use with HTTP cookies. If any
141
117
  # secure attributes have changed since the session was instantiated, compute
142
118
  # a fresh RSA signature.
@@ -168,9 +144,9 @@ module GlobalSession::Session
168
144
  hash['a'] = authority
169
145
  signed_hash = RightSupport::Crypto::SignedHash.new(
170
146
  hash,
171
- :envelope=>true,
172
- :encoding=>GlobalSession::Encoding::JSON,
173
- :private_key=>@directory.private_key)
147
+ @directory.private_key,
148
+ envelope: true,
149
+ encoding: GlobalSession::Encoding::JSON)
174
150
  @signature = signed_hash.sign(@expired_at)
175
151
  end
176
152
 
@@ -183,87 +159,6 @@ module GlobalSession::Session
183
159
  return GlobalSession::Encoding::Base64Cookie.dump(bin)
184
160
  end
185
161
 
186
- # Determine whether any state has changed since the session was loaded.
187
- #
188
- # @return [Boolean] true if something has changed
189
- def dirty?
190
- !!(super || @dirty_secure || @dirty_insecure)
191
- end
192
-
193
- # Return the keys that are currently present in the global session.
194
- #
195
- # === Return
196
- # keys(Array):: List of keys contained in the global session
197
- def keys
198
- @signed.keys + @insecure.keys
199
- end
200
-
201
- # Return the values that are currently present in the global session.
202
- #
203
- # === Return
204
- # values(Array):: List of values contained in the global session
205
- def values
206
- @signed.values + @insecure.values
207
- end
208
-
209
- # Iterate over each key/value pair
210
- #
211
- # === Block
212
- # An iterator which will be called with each key/value pair
213
- #
214
- # === Return
215
- # Returns the value of the last expression evaluated by the block
216
- def each_pair(&block) # :yields: |key, value|
217
- @signed.each_pair(&block)
218
- @insecure.each_pair(&block)
219
- end
220
-
221
- # Lookup a value by its key.
222
- #
223
- # === Parameters
224
- # key(String):: the key
225
- #
226
- # === Return
227
- # value(Object):: The value associated with +key+, or nil if +key+ is not present
228
- def [](key)
229
- key = key.to_s #take care of symbol-style keys
230
- @signed[key] || @insecure[key]
231
- end
232
-
233
- # Set a value in the global session hash. If the supplied key is denoted as
234
- # secure by the global session schema, causes a new signature to be computed
235
- # when the session is next serialized.
236
- #
237
- # === Parameters
238
- # key(String):: The key to set
239
- # value(Object):: The value to set
240
- #
241
- # === Return
242
- # value(Object):: Always returns the value that was set
243
- #
244
- # ===Raise
245
- # InvalidSession:: if the session has been invalidated (and therefore can't be written to)
246
- # ArgumentError:: if the configuration doesn't define the specified key as part of the global session
247
- # NoAuthority:: if the specified key is secure and the local node is not an authority
248
- # UnserializableType:: if the specified value can't be serialized as JSON
249
- def []=(key, value)
250
- key = key.to_s #take care of symbol-style keys
251
- raise GlobalSession::InvalidSession unless valid?
252
-
253
- if @schema_signed.include?(key)
254
- authority_check
255
- @signed[key] = value
256
- @dirty_secure = true
257
- elsif @schema_insecure.include?(key)
258
- @insecure[key] = value
259
- @dirty_insecure = true
260
- else
261
- raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
262
- end
263
-
264
- return value
265
- end
266
-
267
162
  # Return the SHA1 hash of the most recently-computed RSA signature of this session.
268
163
  # This isn't really intended for the end user; it exists so the Web framework integration
269
164
  # code can optimize request speed by caching the most recently verified signature in the
@@ -277,16 +172,6 @@ module GlobalSession::Session
277
172
 
278
173
  private
279
174
 
280
- # This is called by #clone and is used to augment the shallow clone behavior
281
- #
282
- # @return [Object] this global session object which doesn't reference the
283
- # the hashes from the original object
284
- def initialize_copy(source)
285
- super
286
- @signed = ::RightSupport::Data::HashTools.deep_clone2(@signed)
287
- @insecure = ::RightSupport::Data::HashTools.deep_clone2(@insecure)
288
- end
289
-
290
175
  def load_from_cookie(cookie) # :nodoc:
291
176
  hash = nil
292
177
 
@@ -311,20 +196,20 @@ module GlobalSession::Session
311
196
  if @directory.trusted_authority?(authority)
312
197
  signed_hash = RightSupport::Crypto::SignedHash.new(
313
198
  hash,
199
+ @directory.authorities[authority],
314
200
  :envelope=>true,
315
- :encoding=>GlobalSession::Encoding::JSON,
316
- :public_key=>@directory.authorities[authority])
201
+ :encoding=>GlobalSession::Encoding::JSON)
317
202
 
318
203
  begin
319
204
  signed_hash.verify!(signature, expired_at)
320
205
  rescue RightSupport::Crypto::ExpiredSignature
321
206
  raise GlobalSession::ExpiredSession, "Session expired at #{expired_at}"
322
207
  rescue RightSupport::Crypto::InvalidSignature => e
323
- raise SecurityError, "Global session signature verification failed: " + e.message
208
+ raise GlobalSession::InvalidSignature, "Global session signature verification failed: " + e.message
324
209
  end
325
210
 
326
211
  else
327
- raise SecurityError, "Global sessions signed by #{authority.inspect} are not trusted"
212
+ raise GlobalSession::InvalidSignature, "Global sessions signed by #{authority.inspect} are not trusted"
328
213
  end
329
214
 
330
215
  #Check expiration
@@ -349,8 +234,6 @@ module GlobalSession::Session
349
234
  end
350
235
 
351
236
  def create_from_scratch # :nodoc:
352
- authority_check
353
-
354
237
  @signed = {}
355
238
  @insecure = {}
356
239
  @created_at = Time.now.utc
@@ -359,15 +242,6 @@ module GlobalSession::Session
359
242
  renew!
360
243
  end
361
244
 
362
- def create_invalid # :nodoc:
363
- @id = nil
364
- @created_at = Time.now.utc
365
- @expired_at = created_at
366
- @signed = {}
367
- @insecure = {}
368
- @authority = nil
369
- end
370
-
371
245
  # Transform a V1-style attribute hash to an Array with fixed placement for
372
246
  # each element. The V3 scheme is serialized as an array to save space.
373
247
  #
@@ -0,0 +1,140 @@
1
+ require 'securerandom'
2
+
3
+ module GlobalSession::Session
4
+ # Version 4 is based on JSON Web Token; in fact, if there is no insecure
5
+ # state, then a V4 session _is_ a JWT. Otherwise, it's a JWT with a
6
+ # nonstandard fourth component containing the insecure state.
7
+ class V4 < Abstract
8
+ EXPIRED_AT = 'exp'.freeze
9
+ ID = 'id'.freeze
10
+ ISSUED_AT = 'iat'.freeze
11
+ ISSUER = 'iss'.freeze
12
+ NOT_BEFORE = 'nbf'.freeze
13
+
14
+ # Pattern that matches strings that are probably a V4 session cookie.
15
+ HEADER = /^eyJ0eXAiOiJKV1QiL/
16
+
17
+ def self.decode_cookie(cookie)
18
+ header, payload, sig, insec = cookie.split('.')
19
+ header, payload, insec = [header, payload, insec].
20
+ map { |c| c && RightSupport::Data::Base64URL.decode(c) }.
21
+ map { |j| j && GlobalSession::Encoding::JSON.load(j) }
22
+ sig = RightSupport::Data::Base64URL.decode(sig)
23
+ insec ||= {}
24
+
25
+ unless Hash === header && header['typ'] == 'JWT'
26
+ raise GlobalSession::MalformedCookie, "JWT header not present"
27
+ end
28
+ unless Hash === payload
29
+ raise GlobalSession::MalformedCookie, "JWT payload not present"
30
+ end
31
+
32
+ [header, payload, sig, insec]
33
+ rescue JSON::ParserError => e
34
+ raise GlobalSession::MalformedCookie, e.message
35
+ end
36
+
37
+ # Serialize the session. If any secure attributes have changed since the
38
+ # session was instantiated, compute a fresh RSA signature.
39
+ #
40
+ # @return [String]
41
+ def to_s
42
+ if @cookie && !dirty?
43
+ # nothing has changed; just return cached cookie
44
+ return @cookie
45
+ end
46
+
47
+ unless @insecure.nil? || @insecure.empty?
48
+ insec = GlobalSession::Encoding::JSON.dump(@insecure)
49
+ insec = RightSupport::Data::Base64URL.encode(insec)
50
+ end
51
+
52
+ if @signature && !(@dirty_timestamps || @dirty_secure)
53
+ # secure state hasn't changed; reuse JWT piece of cookie
54
+ jwt = @cookie.split('.')[0..2].join('.')
55
+ else
56
+ # secure state has changed; recompute signature & make new JWT
57
+ authority_check
58
+
59
+ payload = @signed.dup
60
+ payload[ID] = id
61
+ payload[EXPIRED_AT] = @expired_at.to_i
62
+ payload[ISSUED_AT] = @created_at.to_i
63
+ payload[ISSUER] = @directory.local_authority_name
64
+
65
+ sh = RightSupport::Crypto::SignedHash.new(payload, @directory.private_key, envelope: :jwt)
66
+ jwt = sh.to_jwt(@expired_at)
67
+ end
68
+
69
+ if insec && !insec.empty?
70
+ return "#{jwt}.#{insec}"
71
+ else
72
+ return jwt
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def load_from_cookie(cookie)
79
+ # Get the basic facts
80
+ header, payload, sig, insec = self.class.decode_cookie(cookie)
81
+ id = payload[ID]
82
+ created_at = payload[ISSUED_AT]
83
+ issuer = payload[ISSUER]
84
+ expired_at = payload[EXPIRED_AT]
85
+ not_before = payload[NOT_BEFORE]
86
+ raise GlobalSession::InvalidSignature, "JWT iat claim missing/wrong" unless Integer === created_at
87
+ raise GlobalSession::InvalidSignature, "JWT iat claim missing/wrong" unless Integer === expired_at
88
+ created_at = Time.at(created_at)
89
+ expired_at = Time.at(expired_at)
90
+ if Numeric === not_before
91
+ not_before = Time.at(not_before)
92
+ raise GlobalSession::PrematureSession, "Session not valid before #{not_before}" unless Time.now >= not_before
93
+ end
94
+
95
+ #Check trust in signing authority
96
+ if @directory.trusted_authority?(issuer)
97
+ signed_hash =
98
+ RightSupport::Crypto::SignedHash.new(payload,
99
+ @directory.authorities[issuer],
100
+ envelope: :jwt
101
+ )
102
+
103
+ begin
104
+ signed_hash.verify!(sig, expired_at)
105
+ rescue RightSupport::Crypto::ExpiredSignature
106
+ raise GlobalSession::ExpiredSession, "Session expired at #{expired_at}"
107
+ rescue RightSupport::Crypto::InvalidSignature => e
108
+ raise GlobalSession::InvalidSignature, "Global session signature verification failed: " + e.message
109
+ end
110
+
111
+ else
112
+ raise GlobalSession::InvalidSignature, "Global sessions signed by #{authority.inspect} are not trusted"
113
+ end
114
+
115
+ #Check other validity (delegate to directory)
116
+ unless @directory.valid_session?(id, expired_at)
117
+ raise GlobalSession::InvalidSession, "Global session has been invalidated"
118
+ end
119
+
120
+ #If all validation stuff passed, assign our instance variables.
121
+ @id = id
122
+ @authority = issuer
123
+ @created_at = created_at
124
+ @expired_at = expired_at
125
+ @signed = payload
126
+ @insecure = insec
127
+ @signature = sig
128
+ @cookie = cookie
129
+ end
130
+
131
+ def create_from_scratch
132
+ @signed = {}
133
+ @insecure = {}
134
+ @created_at = Time.now.utc
135
+ @authority = @directory.local_authority_name
136
+ @id = generate_id
137
+ renew!
138
+ end
139
+ end
140
+ end