app_identity 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rdoc_options +28 -0
- data/Changelog.md +5 -0
- data/Contributing.md +70 -0
- data/Licence.md +25 -0
- data/Manifest.txt +30 -0
- data/README.md +70 -0
- data/Rakefile +50 -0
- data/bin/app-identity-suite-ruby +12 -0
- data/lib/app_identity/app.rb +214 -0
- data/lib/app_identity/error.rb +42 -0
- data/lib/app_identity/faraday_middleware.rb +94 -0
- data/lib/app_identity/internal.rb +130 -0
- data/lib/app_identity/rack_middleware.rb +242 -0
- data/lib/app_identity/validation.rb +83 -0
- data/lib/app_identity/versions.rb +194 -0
- data/lib/app_identity.rb +233 -0
- data/licences/APACHE-2.0.txt +168 -0
- data/licences/DCO.txt +34 -0
- data/spec.md +409 -0
- data/support/app_identity/suite/generator.rb +242 -0
- data/support/app_identity/suite/optional.json +491 -0
- data/support/app_identity/suite/program.rb +204 -0
- data/support/app_identity/suite/required.json +514 -0
- data/support/app_identity/suite/runner.rb +132 -0
- data/support/app_identity/suite.rb +10 -0
- data/support/app_identity/support.rb +119 -0
- data/test/minitest_helper.rb +24 -0
- data/test/test_app_identity.rb +124 -0
- data/test/test_app_identity_app.rb +64 -0
- data/test/test_app_identity_rack_middleware.rb +90 -0
- metadata +306 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "error"
|
4
|
+
require_relative "validation"
|
5
|
+
require_relative "versions"
|
6
|
+
|
7
|
+
class AppIdentity::Internal # :nodoc:
|
8
|
+
include AppIdentity::Validation
|
9
|
+
|
10
|
+
class << self
|
11
|
+
private :new
|
12
|
+
|
13
|
+
def instance
|
14
|
+
@instance ||= new
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_proof!(app, **kwargs)
|
18
|
+
instance.generate_proof!(app, **kwargs)
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_proof!(proof)
|
22
|
+
instance.parse_proof!(proof)
|
23
|
+
end
|
24
|
+
|
25
|
+
def verify_proof!(proof, app, **kwargs)
|
26
|
+
instance.verify_proof!(proof, app, **kwargs)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def generate_proof!(app, nonce: nil, version: nil, disallowed: nil)
|
31
|
+
app = AppIdentity::App.new(app)
|
32
|
+
version ||= app.version
|
33
|
+
nonce ||= app.generate_nonce(version)
|
34
|
+
|
35
|
+
__generate_proof(app, nonce, version, disallowed: disallowed)
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_proof!(proof)
|
39
|
+
return proof if proof.is_a?(Hash)
|
40
|
+
|
41
|
+
raise AppIdentity::Error, "proof must be a string or a map" unless proof.is_a?(String)
|
42
|
+
|
43
|
+
parts = Base64.decode64(proof).split(":", -1)
|
44
|
+
|
45
|
+
case parts.length
|
46
|
+
when 4
|
47
|
+
version, id, nonce, padlock = parts
|
48
|
+
|
49
|
+
version = validate_version(version)
|
50
|
+
AppIdentity::Versions.allowed!(version)
|
51
|
+
|
52
|
+
{version: version, id: id, nonce: nonce, padlock: padlock}
|
53
|
+
when 3
|
54
|
+
id, nonce, padlock = parts
|
55
|
+
|
56
|
+
{version: 1, id: id, nonce: nonce, padlock: padlock}
|
57
|
+
else
|
58
|
+
raise AppIdentity::Error, "proof must have 3 parts (version 1) or 4 parts (any version)"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def verify_proof!(proof, app, disallowed: nil)
|
63
|
+
proof = parse_proof!(proof)
|
64
|
+
|
65
|
+
app = app.call(proof) if app.respond_to?(:call)
|
66
|
+
app = AppIdentity::App.new(app)
|
67
|
+
|
68
|
+
raise AppIdentity::Error, "proof and app do not match" unless app.id == proof[:id]
|
69
|
+
raise AppIdentity::Error, "proof and app version mismatch" if app.version > proof[:version]
|
70
|
+
AppIdentity::Versions.allowed!(proof[:version], disallowed)
|
71
|
+
|
72
|
+
valid_nonce!(proof[:nonce], proof[:version], app.config)
|
73
|
+
validate_padlock(proof[:padlock])
|
74
|
+
|
75
|
+
padlock = __generate_padlock(app, proof[:nonce], proof[:version])
|
76
|
+
|
77
|
+
compare_padlocks(padlock, proof[:padlock]) ? app.verify : nil
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def __generate_proof(app, nonce, version, disallowed: nil)
|
83
|
+
AppIdentity::Versions.allowed!(version, disallowed)
|
84
|
+
|
85
|
+
padlock = __generate_padlock(app, nonce, version)
|
86
|
+
|
87
|
+
return unless padlock
|
88
|
+
|
89
|
+
parts =
|
90
|
+
case version
|
91
|
+
when 1
|
92
|
+
[app.id, nonce, padlock]
|
93
|
+
else
|
94
|
+
[version, app.id, nonce, padlock]
|
95
|
+
end
|
96
|
+
|
97
|
+
Base64.urlsafe_encode64(parts.join(":"))
|
98
|
+
end
|
99
|
+
|
100
|
+
def __generate_padlock(app, nonce, version)
|
101
|
+
versions_compatible!(app.version, version)
|
102
|
+
valid_nonce!(nonce, version, app.config)
|
103
|
+
AppIdentity::Versions[version].make_digest([app.id, nonce, app.secret.call].join(":"))
|
104
|
+
end
|
105
|
+
|
106
|
+
def versions_compatible!(app_version, version)
|
107
|
+
return if app_version <= version
|
108
|
+
raise AppIdentity::Error, "app version #{app_version} is not compatible with proof version #{version}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def valid_nonce!(nonce, version, config)
|
112
|
+
AppIdentity::Versions[version].check_nonce!(nonce, config)
|
113
|
+
end
|
114
|
+
|
115
|
+
def compare_padlocks(generated, provided)
|
116
|
+
return false if generated.nil? || generated.empty?
|
117
|
+
return false if provided.nil? || provided.empty?
|
118
|
+
return false if generated.length != provided.length
|
119
|
+
|
120
|
+
generated = generated.upcase.unpack("C*")
|
121
|
+
provided = provided.upcase.unpack("C*")
|
122
|
+
|
123
|
+
generated.zip(provided).map { |a, b| a ^ b }.reduce(:+) == 0
|
124
|
+
end
|
125
|
+
|
126
|
+
def generate_version_nonce(version)
|
127
|
+
version = validate_version(version)
|
128
|
+
AppIdentity[validate_version(version)][:nonce].call
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require "app_identity"
|
5
|
+
require "app_identity/error"
|
6
|
+
require "set"
|
7
|
+
|
8
|
+
# A Rack middleware that verifies App Identity proofs provided via one or more
|
9
|
+
# HTTP headers.
|
10
|
+
#
|
11
|
+
# When multiple proof values are provided in the request, all must be
|
12
|
+
# successfully verified. If any of the proof values cannot be verified, request
|
13
|
+
# processing halts with `403 Forbidden`. Should no proof headers are included,
|
14
|
+
# the request is considered invalid.
|
15
|
+
#
|
16
|
+
# All of the above behaviours can be modified through configuration (see below).
|
17
|
+
#
|
18
|
+
# The results of completed proof validations can be found at
|
19
|
+
# env["app_identity"], regardless of success or failure.
|
20
|
+
#
|
21
|
+
# ### Configuration
|
22
|
+
#
|
23
|
+
# The Rack middlware can be configured with the following options:
|
24
|
+
#
|
25
|
+
# - `apps`: A list of AppIdentity::App objects or objects that can be converted
|
26
|
+
# into AppIdentity::App objects to be used for proof validation. Duplicate
|
27
|
+
# values will be ignored.
|
28
|
+
#
|
29
|
+
# - `disallowed`: A list of algorithm versions that are not allowed when
|
30
|
+
# processing received identity proofs. See AppIdentity::Versions.allowed?.
|
31
|
+
#
|
32
|
+
# - `finder`: A 1-arity callable function that will load an input to
|
33
|
+
# AppIdentity::App.new from an external source given a parsed proof.
|
34
|
+
#
|
35
|
+
# - `headers`: A list of HTTP header names.
|
36
|
+
#
|
37
|
+
# - `on_failure`: The behaviour of the Rack middleware when proof validation
|
38
|
+
# fails. Must be one of the following values:
|
39
|
+
#
|
40
|
+
# - `:forbidden`: Halt request processing and respond with a `403`
|
41
|
+
# (forbidden) status. This is the same as `[:halt, :forbidden]`. This is
|
42
|
+
# the default `on_failure` behaviour.
|
43
|
+
#
|
44
|
+
# - `[:halt, status]`: Halt request processing and return the specified
|
45
|
+
# status code. An empty body is emitted.
|
46
|
+
#
|
47
|
+
# - `[:halt, status, body]`: Halt request processing and return the
|
48
|
+
# specified status code. The body value is included in the response.
|
49
|
+
#
|
50
|
+
# - `:continue`: Continue processing, ensuring that failure states are
|
51
|
+
# recorded for the application to act on at a later point. This could be
|
52
|
+
# used to implement a distinction between *validating* a proof and
|
53
|
+
# *requiring* that the proof is valid.
|
54
|
+
#
|
55
|
+
# - A 1-arity callable accepting the Rack `env` value and returns one of the
|
56
|
+
# above values.
|
57
|
+
#
|
58
|
+
# At least one of `apps` or `finder` **must** be supplied. If both are present,
|
59
|
+
# apps are looked up in the `apps` list first.
|
60
|
+
#
|
61
|
+
# ```ruby
|
62
|
+
# use AppIdentity::RackMiddleware, header: "application-identity",
|
63
|
+
# finder: ->(proof) { ApplicationModel.find(proof[:id]) }
|
64
|
+
# ```
|
65
|
+
class AppIdentity::RackMiddleware
|
66
|
+
def initialize(app, options = {}) # :nodoc:
|
67
|
+
@app = app
|
68
|
+
|
69
|
+
if !options.has_key?(:apps) && !options.has_key?(:finder)
|
70
|
+
raise AppIdentity::Error, :plug_missing_apps_or_finder
|
71
|
+
end
|
72
|
+
|
73
|
+
@apps = get_apps(options)
|
74
|
+
@finder = options[:finder]
|
75
|
+
|
76
|
+
if @apps.empty? && @finder.nil?
|
77
|
+
raise "One of `apps` or `finder` options is required."
|
78
|
+
end
|
79
|
+
|
80
|
+
@disallowed = get_disallowed(options)
|
81
|
+
@headers = get_headers(options)
|
82
|
+
@on_failure = get_on_failure(options)
|
83
|
+
end
|
84
|
+
|
85
|
+
def call(env) # :nodoc:
|
86
|
+
headers = verify_headers(Hash[*@headers.flat_map { |h| [h, env[h]] }])
|
87
|
+
|
88
|
+
env["app_identity"] = headers
|
89
|
+
|
90
|
+
if has_errors?(headers)
|
91
|
+
dispatch_on_failure(@on_failure, env)
|
92
|
+
else
|
93
|
+
@app.call(env)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def dispatch_on_failure(on_failure, env)
|
100
|
+
if on_failure.respond_to?(:call)
|
101
|
+
dispatch_on_failure(on_failure.call(env), env)
|
102
|
+
elsif on_failure == :forbidden
|
103
|
+
halt
|
104
|
+
elsif on_failure == :continue
|
105
|
+
@app.call(env)
|
106
|
+
elsif on_failure.is_a?(Array) && on_failure.first == :halt
|
107
|
+
_, status, body = on_failure
|
108
|
+
halt(status, body)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def has_errors?(headers)
|
113
|
+
headers.empty? || headers.any? { |_k, v| v.nil? || v.empty? || v.any? { |vv| vv.nil? || !vv.verified } }
|
114
|
+
end
|
115
|
+
|
116
|
+
def halt(status = nil, body = nil)
|
117
|
+
status ||= :forbidden
|
118
|
+
status = Rack::Utils.status_code(status) if status.is_a?(Symbol)
|
119
|
+
|
120
|
+
body = Array(body)
|
121
|
+
|
122
|
+
length = body.empty? ? "0" : body.length.to_s
|
123
|
+
|
124
|
+
[status, {"Content-Type" => "text/plain", "Content-Length" => length}, body]
|
125
|
+
end
|
126
|
+
|
127
|
+
def verify_headers(headers)
|
128
|
+
headers.each_with_object({}) { |(header, values), result|
|
129
|
+
next if values.nil? || values.empty?
|
130
|
+
|
131
|
+
# If Rack can start returning arrays, we are ready.
|
132
|
+
|
133
|
+
result[header] = Array(values).each_with_object([]) { |value, list|
|
134
|
+
break list if verify_header_value(value, list) == :halt
|
135
|
+
}
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
def verify_header_value(value, list)
|
140
|
+
proof = AppIdentity.parse_proof(value)
|
141
|
+
return handle_proof_error(list, nil) unless proof
|
142
|
+
|
143
|
+
app = @apps[proof[:id]]
|
144
|
+
|
145
|
+
if app.nil? && @finder
|
146
|
+
app = AppIdentity::App.new(finder.call(proof))
|
147
|
+
@apps[app.id] = app
|
148
|
+
end
|
149
|
+
|
150
|
+
return handle_proof_error(list, nil) unless app
|
151
|
+
|
152
|
+
verified = AppIdentity.verify_proof(proof, app, disallowed: @disallowed)
|
153
|
+
|
154
|
+
if verified
|
155
|
+
list << verified
|
156
|
+
:cont
|
157
|
+
else
|
158
|
+
handle_proof_error(list, verified)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def handle_proof_error(list, value)
|
163
|
+
case @on_failure
|
164
|
+
when :continue
|
165
|
+
list << value
|
166
|
+
:cont
|
167
|
+
when :forbidden, Array
|
168
|
+
:halt
|
169
|
+
else
|
170
|
+
list << value
|
171
|
+
:cont
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def get_request_headers(env)
|
176
|
+
Hash[*@headers.lazy.flat_map { |header| [header, env[header]] }]
|
177
|
+
end
|
178
|
+
|
179
|
+
def get_apps(options)
|
180
|
+
options.fetch(:apps, []).each_with_object({}) { |input, map|
|
181
|
+
app = AppIdentity::App.new(input)
|
182
|
+
map[app.id] = app unless map.key?(app.id)
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
186
|
+
def get_disallowed(options)
|
187
|
+
disallowed = options[:disallowed] || []
|
188
|
+
|
189
|
+
if disallowed.is_a?(Set)
|
190
|
+
disallowed
|
191
|
+
elsif disallowed.is_a?(Array)
|
192
|
+
Set.new(disallowed)
|
193
|
+
else
|
194
|
+
raise AppIdentity::Error, :plug_disallowed_invalid
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def get_headers(options)
|
199
|
+
headers = options[:headers] || []
|
200
|
+
|
201
|
+
raise AppIdentity::Error, :plug_headers_required if !headers.is_a?(Array) || headers.empty?
|
202
|
+
|
203
|
+
headers.map { |header| parse_option_header(header) }
|
204
|
+
end
|
205
|
+
|
206
|
+
def get_on_failure(options)
|
207
|
+
resolve_on_failure_option(options[:on_failure] || :forbidden)
|
208
|
+
end
|
209
|
+
|
210
|
+
def parse_option_header(header)
|
211
|
+
if !header.is_a?(String) || header.empty?
|
212
|
+
raise AppIdentity::Error, :plug_header_invalid
|
213
|
+
end
|
214
|
+
|
215
|
+
if /\AHTTP_[A-Z_0-9]+\z/.match?(header)
|
216
|
+
header
|
217
|
+
else
|
218
|
+
-"HTTP_#{header.to_s.tr("-", "_").upcase}"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def resolve_on_failure_option(value)
|
223
|
+
valid =
|
224
|
+
case value
|
225
|
+
when :forbidden, :continue
|
226
|
+
true
|
227
|
+
when Array
|
228
|
+
case [value.first, value.length, value[1].class]
|
229
|
+
when [:halt, 2, Symbol], [:halt, 2, Integer], [:halt, 3, Symbol], [:halt, 3, Integer]
|
230
|
+
true
|
231
|
+
end
|
232
|
+
else
|
233
|
+
value.respond_to?(:call)
|
234
|
+
end
|
235
|
+
|
236
|
+
if valid
|
237
|
+
value
|
238
|
+
else
|
239
|
+
raise AppIdentity::Error, :plug_on_failure_invalid
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "error"
|
4
|
+
|
5
|
+
class AppIdentity
|
6
|
+
module Validation # :nodoc:
|
7
|
+
def validate_id(id) # :nodoc:
|
8
|
+
id.tap {
|
9
|
+
validate_not_nil(:id, id)
|
10
|
+
validate_not_empty(:id, id.to_s)
|
11
|
+
validate_no_colons(:id, id.to_s)
|
12
|
+
}.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate_secret(secret) # :nodoc:
|
16
|
+
secret.tap {
|
17
|
+
secret = secret.call if secret.respond_to?(:call)
|
18
|
+
|
19
|
+
validate_not_nil(:secret, secret)
|
20
|
+
raise AppIdentity::Error, "secret must be a binary string value" unless secret.is_a?(String)
|
21
|
+
validate_not_empty(:secret, secret)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_version(version) # :nodoc:
|
26
|
+
version.tap {
|
27
|
+
validate_not_nil(:version, version)
|
28
|
+
|
29
|
+
begin
|
30
|
+
version = Integer(version) if version.is_a?(String)
|
31
|
+
rescue
|
32
|
+
raise AppIdentity::Error, "version cannot be converted to an integer"
|
33
|
+
end
|
34
|
+
|
35
|
+
if !version.is_a?(Integer) || version <= 0
|
36
|
+
raise AppIdentity::Error, "version must be a positive integer"
|
37
|
+
end
|
38
|
+
}.to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_config(config) # :nodoc:
|
42
|
+
config.tap {
|
43
|
+
raise AppIdentity::Error, "config must be nil or a map" unless config.nil? || config.is_a?(Hash)
|
44
|
+
|
45
|
+
if config.is_a?(Hash)
|
46
|
+
fuzz = config[:fuzz] || config["fuzz"]
|
47
|
+
|
48
|
+
case fuzz
|
49
|
+
when nil
|
50
|
+
nil
|
51
|
+
when Integer
|
52
|
+
raise AppIdentity::Error, "config.fuzz must be a positive integer or nil" unless fuzz > 0
|
53
|
+
else
|
54
|
+
raise AppIdentity::Error, "config.fuzz must be a positive integer or nil"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_padlock(padlock) # :nodoc:
|
61
|
+
padlock.tap {
|
62
|
+
validate_not_nil(:padlock, padlock)
|
63
|
+
raise AppIdentity::Error, "padlock must be a string" unless padlock.is_a?(String)
|
64
|
+
validate_not_empty(:padlock, padlock)
|
65
|
+
validate_no_colons(:padlock, padlock)
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def validate_not_nil(type, value)
|
72
|
+
raise AppIdentity::Error, "#{type} must not be nil" if value.nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_not_empty(type, value)
|
76
|
+
raise AppIdentity::Error, "#{type} must not be an empty string" if value.empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_no_colons(type, value)
|
80
|
+
raise AppIdentity::Error, "#{type} must not contain colons" if /:/.match?(value)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
require "digest/sha2"
|
5
|
+
require "securerandom"
|
6
|
+
require "set"
|
7
|
+
|
8
|
+
require_relative "validation"
|
9
|
+
require_relative "error"
|
10
|
+
|
11
|
+
class AppIdentity
|
12
|
+
module Versions # :nodoc:
|
13
|
+
class Base # :nodoc:
|
14
|
+
class << self
|
15
|
+
def defined # :nodoc:
|
16
|
+
@defined ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def instance # :nodoc:
|
20
|
+
@_instance = new
|
21
|
+
end
|
22
|
+
|
23
|
+
def inherited(subclass) # :nodoc:
|
24
|
+
match = /V(?<version>\d+)\z/.match(subclass.name)
|
25
|
+
return unless match
|
26
|
+
version = match[:version].to_i
|
27
|
+
|
28
|
+
raise AppIdentity::Error, "version is not unique" if Base.defined.has_key?(version)
|
29
|
+
|
30
|
+
subclass.define_method(:version) { version }
|
31
|
+
|
32
|
+
Base.defined[version] = subclass.instance
|
33
|
+
end
|
34
|
+
|
35
|
+
private :new
|
36
|
+
end
|
37
|
+
|
38
|
+
def make_digest(raw) # :nodoc:
|
39
|
+
digest_algorithm.hexdigest(raw).upcase
|
40
|
+
end
|
41
|
+
|
42
|
+
def inspect # :nodoc:
|
43
|
+
parts = [self.class]
|
44
|
+
parts << "version=#{version}" if respond_to?(:version)
|
45
|
+
parts << "nonce=#{nonce_type}" if respond_to?(:nonce_type)
|
46
|
+
parts << "digest=#{digest_algorithm}" if respond_to?(:digest_algorithm)
|
47
|
+
"#<#{parts.join(" ")}>"
|
48
|
+
end
|
49
|
+
|
50
|
+
def check_nonce!(nonce, _config) # :nodoc:
|
51
|
+
raise AppIdentity::Error, "nonce must not be nil" if nonce.nil?
|
52
|
+
raise AppIdentity::Error, "nonce must be a string" unless nonce.is_a?(String)
|
53
|
+
raise AppIdentity::Error, "nonce must not be blank" if nonce.empty?
|
54
|
+
raise AppIdentity::Error, "nonce must not contain colon characters" if /:/.match?(nonce)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class RandomNonce < Base # :nodoc:
|
59
|
+
def nonce_type # :nodoc:
|
60
|
+
:random
|
61
|
+
end
|
62
|
+
|
63
|
+
def generate_nonce # :nodoc:
|
64
|
+
SecureRandom.urlsafe_base64(32)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class TimestampNonce < Base # :nodoc:
|
69
|
+
def nonce_type # :nodoc:
|
70
|
+
:timestamp
|
71
|
+
end
|
72
|
+
|
73
|
+
def generate_nonce # :nodoc:
|
74
|
+
Time.now.utc.strftime("%Y%m%dT%H%M%S.%6NZ")
|
75
|
+
end
|
76
|
+
|
77
|
+
def check_nonce!(nonce, config) # :nodoc:
|
78
|
+
super(nonce, config)
|
79
|
+
|
80
|
+
timestamp = parse_timestamp!(nonce)
|
81
|
+
config ||= default_config
|
82
|
+
fuzz = config[:fuzz] || config["fuzz"] || 600
|
83
|
+
|
84
|
+
diff = (Time.now.utc - timestamp).abs.to_i
|
85
|
+
|
86
|
+
raise AppIdentity::Error, "nonce is invalid" unless diff <= fuzz
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def default_config
|
92
|
+
{fuzz: 600}
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse_timestamp!(nonce)
|
96
|
+
Time.strptime(nonce, "%Y%m%dT%H%M%S.%N%Z").tap { |value| raise if value.nil? }.utc
|
97
|
+
rescue
|
98
|
+
raise AppIdentity::Error, "nonce does not look like a timestamp"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# V1 is the original algorithm, using a permanent nonce value and SHA256
|
103
|
+
# digests. The use of this version is strongly discouraged for new clients.
|
104
|
+
class V1 < RandomNonce # :nodoc:
|
105
|
+
def digest_algorithm # :nodoc:
|
106
|
+
Digest::SHA256
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# V2 uses a timestamp-based nonce value with SHA256 digests.
|
111
|
+
#
|
112
|
+
# The nonce values will be verified to be within plus or minus a configured
|
113
|
+
# number of seconds.
|
114
|
+
class V2 < TimestampNonce # :nodoc:
|
115
|
+
def digest_algorithm # :nodoc:
|
116
|
+
Digest::SHA256
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# V3 uses a timestamp-based nonce value with SHA384 digests.
|
121
|
+
#
|
122
|
+
# The nonce values will be verified to be within plus or minus a configured
|
123
|
+
# number of seconds.
|
124
|
+
class V3 < TimestampNonce # :nodoc:
|
125
|
+
def digest_algorithm # :nodoc:
|
126
|
+
Digest::SHA384
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# V4 uses a timestamp-based nonce value with SHA512 digests.
|
131
|
+
#
|
132
|
+
# The nonce values will be verified to be within plus or minus a configured
|
133
|
+
# number of seconds.
|
134
|
+
class V4 < TimestampNonce # :nodoc:
|
135
|
+
def digest_algorithm # :nodoc:
|
136
|
+
Digest::SHA512
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class << self
|
141
|
+
include AppIdentity::Validation
|
142
|
+
|
143
|
+
# Looks up the version instance by version.
|
144
|
+
def [](version) # :nodoc:
|
145
|
+
AppIdentity::Versions::Base.defined.fetch(version)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Checks to see if the version has been defined.
|
149
|
+
def valid?(version) # :nodoc:
|
150
|
+
AppIdentity::Versions::Base.defined.has_key?(version)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Tests that the version is valid or raises an exception.
|
154
|
+
def valid!(version) # :nodoc:
|
155
|
+
return true if valid?(version)
|
156
|
+
raise AppIdentity::Error, "version must be one of #{AppIdentity::Versions::Base.defined.keys}"
|
157
|
+
end
|
158
|
+
|
159
|
+
# Checks to see if the version is valid and not explicitly disallowed,
|
160
|
+
# either in the provided list or in global list.
|
161
|
+
def allowed?(version, provided = nil)
|
162
|
+
valid?(version) && !disallowed.member?(version) &&
|
163
|
+
!Set.new(provided).member?(version)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Tests that the version is valid and not explicitly disallowed or raises
|
167
|
+
# an exception.
|
168
|
+
def allowed!(version, provided = nil)
|
169
|
+
return true if valid!(version) && allowed?(version, provided)
|
170
|
+
raise AppIdentity::Error, "version #{version} has been disallowed"
|
171
|
+
end
|
172
|
+
|
173
|
+
# Globally disallow the listed versions.
|
174
|
+
def disallow(*versions)
|
175
|
+
disallowed.merge(coerce_versions(versions))
|
176
|
+
end
|
177
|
+
|
178
|
+
# Globally allow the listed versions.
|
179
|
+
def allow(*versions)
|
180
|
+
disallowed.subtract(coerce_versions(versions))
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
def coerce_versions(versions)
|
186
|
+
versions.select { |v| v.is_a?(Integer) }
|
187
|
+
end
|
188
|
+
|
189
|
+
def disallowed
|
190
|
+
@disalllowed ||= Set.new
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|