app_identity 1.0.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.
- 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
|