Class: GlobalSession::Session::V3

Inherits:
Abstract
  • Object
show all
Defined in:
lib/global_session/session/v3.rb

Overview

Global session V3 uses JSON serialization, no compression, and a detached signature that is excluded from the JSON structure for efficiency reasons.

The binary structure of a V3 session looks like this:

<utf8_json><0x00><binary_signature>

Its JSON structure is an Array with the following format:

[<version_integer>,
 <uuid_string>,
 <signing_authority_string>,
 <creation_timestamp_integer>,
 <expiration_timestamp_integer>,
 {<signed_data_hash>},
 {<unsigned_data_hash>}]

The design goal of V3 is to ensure broad compatibility across various programming languages and cryptographic libraries, and to create a serialization format that can be reused for future versions. To this end, it sacrifices space efficiency by switching back to JSON encoding (instead of msgpack), and uses the undocumented OpenSSL::PKey#sign and #verify operations which rely on the PKCS7-compliant OpenSSL EVP API.

Constant Summary collapse

STRING_ENCODING =
!!(RUBY_VERSION !~ /1.8/)

Instance Attribute Summary

Attributes inherited from Abstract

#authority, #created_at, #directory, #expired_at, #id, #insecure, #signed

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Abstract

#has_key?, #initialize, #inspect, #invalidate!, #new_record?, #renew!, #supports_key?, #to_h, #valid?

Constructor Details

This class inherits a constructor from GlobalSession::Session::Abstract

Class Method Details

Utility method to decode a cookie; good for console debugging. This performs no validation or security check of any sort.

Parameters

cookie(String)

well-formed global session cookie



54
55
56
57
58
# File 'lib/global_session/session/v3.rb', line 54

def self.decode_cookie(cookie)
  bin = GlobalSession::Encoding::Base64Cookie.load(cookie)
  json, sig = split_body(bin)
  return GlobalSession::Encoding::JSON.load(json), sig
end

.join_body(json, signature) ⇒ String

Join a UTF-8 JSON document and an ASCII-8bit binary string.

This is an implementation helper for GlobalSession serialization and not useful for the public at large. It’s left public as an aid for those who want to hack sessions.

Parameters:

  • json (String)

    a UTF-8 JSON document (encoding will be forced to UTF_8!)

  • signature (String)

    a binary signautre (encoding will be forced to ASCII_8BIT!)

Returns:

  • (String)

    a binary concatenation of the two inputs, separated by 0x00



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/global_session/session/v3.rb', line 96

def self.join_body(json, signature)
  result = ""
  if STRING_ENCODING
    result.force_encoding(Encoding::ASCII_8BIT)
    json.force_encoding(Encoding::ASCII_8BIT)
    signature.force_encoding(Encoding::ASCII_8BIT)
  end

  result << json
  result << "\x00"
  result << signature
  result
end

.split_body(input) ⇒ Array

Split an ASCII-8bit input string into two constituent parts: a UTF-8 JSON document and an ASCII-8bit binary string. A null (0x00) separator character is presumed to separate the two parts of the input string.

This is an implementation helper for GlobalSession serialization and not useful for the public at large. It’s left public as an aid for those who want to hack sessions.

Parameters:

  • input (String)

    a binary string (encoding will be forced to ASCII_8BIT!)

Returns:

  • (Array)

    returns a 2-element Array of String: json document, plus binary signature

Raises:

  • (ArgumentError)

    if the null separator is missing



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/global_session/session/v3.rb', line 70

def self.split_body(input)
  input.force_encoding(Encoding::ASCII_8BIT) if STRING_ENCODING
  null_at = input.index("\x00")

  if null_at
    json = input[0...null_at]
    sig = input[null_at+1..-1]
    if STRING_ENCODING
      json.force_encoding(Encoding::UTF_8)
      sig.force_encoding(Encoding::ASCII_8BIT)
    end

    return json, sig
  else
    raise ArgumentError, "Malformed input string does not contain 0x00 byte"
  end
end

Instance Method Details

#[](key) ⇒ Object

Lookup a value by its key.

Parameters

key(String)

the key

Return

value(Object)

The value associated with key, or nil if key is not present



225
226
227
228
# File 'lib/global_session/session/v3.rb', line 225

def [](key)
  key = key.to_s #take care of symbol-style keys
  @signed[key] || @insecure[key]
end

#[]=(key, value) ⇒ Object

Set a value in the global session hash. If the supplied key is denoted as secure by the global session schema, causes a new signature to be computed when the session is next serialized.

Parameters

key(String)

The key to set

value(Object)

The value to set

Return

value(Object)

Always returns the value that was set

Raise

InvalidSession

if the session has been invalidated (and therefore can’t be written to)

ArgumentError

if the configuration doesn’t define the specified key as part of the global session

NoAuthority

if the specified key is secure and the local node is not an authority

UnserializableType

if the specified value can’t be serialized as JSON



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/global_session/session/v3.rb', line 246

def []=(key, value)
  key = key.to_s #take care of symbol-style keys
  raise GlobalSession::InvalidSession unless valid?

  if @schema_signed.include?(key)
    authority_check
    @signed[key] = value
    @dirty_secure = true
  elsif @schema_insecure.include?(key)
    @insecure[key] = value
    @dirty_insecure = true
  else
    raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
  end

  return value
end

#delete(key) ⇒ Object

Delete a key from the global session attributes. If the key exists, mark the global session dirty

Parameters:

  • the (String)

    key to delete

Returns:

  • (Object)

    the value of the key deleted, or nil if not found

Raises:



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/global_session/session/v3.rb', line 115

def delete(key)
  key = key.to_s #take care of symbol-style keys
  raise GlobalSession::InvalidSession unless valid?

  if @schema_signed.include?(key)
    authority_check

    # Only mark dirty if the key actually exists
    @dirty_secure = true if @signed.keys.include? key
    value = @signed.delete(key)
  elsif @schema_insecure.include?(key)
    
    # Only mark dirty if the key actually exists
    @dirty_insecure = true if @insecure.keys.include? key
    value = @insecure.delete(key)
  else
    raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
  end
  
  return value
end

#dirty?Boolean

Determine whether any state has changed since the session was loaded.

Returns:

  • (Boolean)

    true if something has changed



186
187
188
# File 'lib/global_session/session/v3.rb', line 186

def dirty?
  !!(super || @dirty_secure || @dirty_insecure)
end

#each_pair(&block) ⇒ Object

Iterate over each key/value pair

Block

An iterator which will be called with each key/value pair

Return

Returns the value of the last expression evaluated by the block



213
214
215
216
# File 'lib/global_session/session/v3.rb', line 213

def each_pair(&block) # :yields: |key, value|
  @signed.each_pair(&block)
  @insecure.each_pair(&block)
end

#keysObject

Return the keys that are currently present in the global session.

Return

keys(Array)

List of keys contained in the global session



194
195
196
# File 'lib/global_session/session/v3.rb', line 194

def keys
  @signed.keys + @insecure.keys
end

#signature_digestObject

Return the SHA1 hash of the most recently-computed RSA signature of this session. This isn’t really intended for the end user; it exists so the Web framework integration code can optimize request speed by caching the most recently verified signature in the local session and avoid re-verifying it on every request.

Return

digest(String)

SHA1 hex-digest of most-recently-computed signature



271
272
273
# File 'lib/global_session/session/v3.rb', line 271

def signature_digest
  @signature ? digest(@signature) : nil
end

#to_sString

Serialize the session to a form suitable for use with HTTP cookies. If any secure attributes have changed since the session was instantiated, compute a fresh RSA signature.

Returns:

  • (String)

    a B64cookie-encoded JSON-serialized global session

Raises:



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/global_session/session/v3.rb', line 143

def to_s
  if @cookie && !dirty?
    #use cached cookie if nothing has changed
    return @cookie
  end

  unless serializable?(@signed) && serializable?(@insecure)
    raise GlobalSession::UnserializableType,
          "Attributes hash contains non-String keys, cannot be cleanly marshalled"
  end

  hash = {'v' => 3,
          'id' => @id, 'a' => @authority,
          'tc' => @created_at.to_i, 'te' => @expired_at.to_i,
          'ds' => @signed}

  if @signature && !dirty?
    #use cached signature unless we've changed secure state
    authority = @authority
  else
    authority_check
    authority = @directory.local_authority_name
    hash['a'] = authority
    signed_hash = RightSupport::Crypto::SignedHash.new(
      hash,
      :envelope=>true,
      :encoding=>GlobalSession::Encoding::JSON,
      :private_key=>@directory.private_key)
    @signature = signed_hash.sign(@expired_at)
  end

  hash['dx'] = @insecure
  hash['a'] = authority

  array = attribute_hash_to_array(hash)
  json = GlobalSession::Encoding::JSON.dump(array)
  bin = self.class.join_body(json, @signature)
  return GlobalSession::Encoding::Base64Cookie.dump(bin)
end

#valuesObject

Return the values that are currently present in the global session.

Return

values(Array)

List of values contained in the global session



202
203
204
# File 'lib/global_session/session/v3.rb', line 202

def values
  @signed.values + @insecure.values
end