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,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require "digest/sha2"
|
5
|
+
|
6
|
+
# This module should only be used by the unit tests and the test suite
|
7
|
+
# generator.
|
8
|
+
module AppIdentity::Support # :nodoc:
|
9
|
+
private
|
10
|
+
|
11
|
+
def make_app(version, fuzz = nil)
|
12
|
+
case version
|
13
|
+
when 1 then AppIdentity::App.new(v1_input(fuzz))
|
14
|
+
when 2 then AppIdentity::App.new(v2_input(fuzz))
|
15
|
+
when 3 then AppIdentity::App.new(v3_input(fuzz))
|
16
|
+
when 4 then AppIdentity::App.new(v4_input(fuzz))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def v1_input(fuzz = nil)
|
21
|
+
{version: 1, id: SecureRandom.uuid, secret: SecureRandom.hex(32)}.tap { |app|
|
22
|
+
app[:config] = {fuzz: fuzz} if fuzz
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def v2_input(fuzz = nil)
|
27
|
+
v1_input(fuzz).merge(version: 2)
|
28
|
+
end
|
29
|
+
|
30
|
+
def v3_input(fuzz = nil)
|
31
|
+
v1_input(fuzz).merge(version: 3)
|
32
|
+
end
|
33
|
+
|
34
|
+
def v4_input(fuzz = nil)
|
35
|
+
v1_input(fuzz).merge(version: 4)
|
36
|
+
end
|
37
|
+
|
38
|
+
def v1(fuzz = nil)
|
39
|
+
@v1 ||= v1_input(fuzz)
|
40
|
+
end
|
41
|
+
|
42
|
+
def v1_app(fuzz = nil)
|
43
|
+
AppIdentity::App.new(v1(fuzz))
|
44
|
+
end
|
45
|
+
|
46
|
+
def v2(fuzz = nil)
|
47
|
+
@v2 ||= v2_input(fuzz)
|
48
|
+
end
|
49
|
+
|
50
|
+
def v2_app(fuzz = nil)
|
51
|
+
AppIdentity::App.new(v2)
|
52
|
+
end
|
53
|
+
|
54
|
+
def v3(fuzz = nil)
|
55
|
+
@v3 ||= v3_input(fuzz)
|
56
|
+
end
|
57
|
+
|
58
|
+
def v3_app(fuzz = nil)
|
59
|
+
AppIdentity::App.new(v3)
|
60
|
+
end
|
61
|
+
|
62
|
+
def v4(fuzz = nil)
|
63
|
+
@v4 ||= v4_input(fuzz)
|
64
|
+
end
|
65
|
+
|
66
|
+
def v4_app(fuzz = nil)
|
67
|
+
AppIdentity::App.new(v4)
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_padlock(app, opts = {})
|
71
|
+
app_id = opts.delete(:id) { app.id }
|
72
|
+
nonce = opts.delete(:nonce) { "nonce" }
|
73
|
+
secret = opts.delete(:secret) { app.secret }
|
74
|
+
secret = secret.call if secret.respond_to?(:call)
|
75
|
+
version = opts.delete(:version) { app.version }
|
76
|
+
|
77
|
+
case version
|
78
|
+
when 1, 2
|
79
|
+
Digest::SHA256.hexdigest([app_id, nonce, secret].join(":")).upcase
|
80
|
+
when 3
|
81
|
+
Digest::SHA384.hexdigest([app_id, nonce, secret].join(":")).upcase
|
82
|
+
when 4
|
83
|
+
Digest::SHA512.hexdigest([app_id, nonce, secret].join(":")).upcase
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_proof(app, padlock, opts = {})
|
88
|
+
app_id = opts.delete(:id) { app.id }
|
89
|
+
nonce = opts.delete(:nonce) { "nonce" }
|
90
|
+
version = opts.delete(:version) { 1 }
|
91
|
+
|
92
|
+
proof = version == 1 ? "#{app_id}:#{nonce}:#{padlock}" : "#{version}:#{app_id}:#{nonce}:#{padlock}"
|
93
|
+
|
94
|
+
Base64.urlsafe_encode64(proof)
|
95
|
+
end
|
96
|
+
|
97
|
+
def decode_to_parts(header)
|
98
|
+
Base64.decode64(header).split(":")
|
99
|
+
end
|
100
|
+
|
101
|
+
def timestamp_nonce(diff = nil, scale = :minutes)
|
102
|
+
ts = adjust(Time.now.utc, diff, scale)
|
103
|
+
|
104
|
+
ts.strftime("%Y%m%dT%H%M%S.%6NZ")
|
105
|
+
end
|
106
|
+
|
107
|
+
def adjust(ts, diff, scale)
|
108
|
+
return ts if diff.nil?
|
109
|
+
|
110
|
+
case scale
|
111
|
+
when :second, :seconds
|
112
|
+
ts + diff
|
113
|
+
when :minute, :minutes
|
114
|
+
ts + (diff * 60)
|
115
|
+
when :hour, :hours
|
116
|
+
ts + (diff * 60 * 60)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../../support", __FILE__))
|
4
|
+
|
5
|
+
gem "minitest"
|
6
|
+
|
7
|
+
require "minitest/autorun"
|
8
|
+
|
9
|
+
require "app_identity"
|
10
|
+
require "app_identity/support"
|
11
|
+
|
12
|
+
module Minitest::AppIdentityExtensions
|
13
|
+
def assert_exception_message(message, *types, &block)
|
14
|
+
msg = types.last if types.last.is_a?(String)
|
15
|
+
|
16
|
+
types = [AppIdentity::Error] if types.empty?
|
17
|
+
|
18
|
+
result = assert_raises(*types, &block)
|
19
|
+
assert_equal message, result.message, msg
|
20
|
+
end
|
21
|
+
|
22
|
+
Minitest::Test.send(:include, self)
|
23
|
+
Minitest::Test.send(:include, AppIdentity::Support)
|
24
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest_helper"
|
4
|
+
|
5
|
+
class TestAppIdentity < Minitest::Test
|
6
|
+
def subject
|
7
|
+
@subject ||= AppIdentity::Internal.send(:new)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_generate_v1_proof
|
11
|
+
proof = subject.generate_proof!(v1)
|
12
|
+
refute_nil proof
|
13
|
+
assert_equal 3, decode_to_parts(proof).length
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_generate_valid_v1_proof
|
17
|
+
proof = subject.generate_proof!(v1)
|
18
|
+
assert_equal v1_app.verify, subject.verify_proof!(proof, v1)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_generate_v2_proof
|
22
|
+
proof = subject.generate_proof!(v2_app)
|
23
|
+
refute_nil proof
|
24
|
+
assert_equal 4, decode_to_parts(proof).length
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_generate_valid_v2_proof
|
28
|
+
proof = subject.generate_proof!(v2)
|
29
|
+
assert_equal v2_app.verify, subject.verify_proof!(proof, v2)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_generate_valid_v3_proof
|
33
|
+
proof = subject.generate_proof!(v3)
|
34
|
+
assert_equal v3_app.verify, subject.verify_proof!(proof, v3)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_verify_fails_if_not_base64
|
38
|
+
assert_exception_message("proof must have 3 parts (version 1) or 4 parts (any version)") {
|
39
|
+
subject.verify_proof!("not base64", v1)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_verify_fail_on_insufficent_parts
|
44
|
+
assert_exception_message("proof must have 3 parts (version 1) or 4 parts (any version)") {
|
45
|
+
subject.verify_proof!(Base64.urlsafe_encode64("a:b"), v1)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_verify_fail_on_bad_v1_nonce
|
50
|
+
padlock = build_padlock(v1_app, nonce: "n:once")
|
51
|
+
proof = build_proof(v1_app, padlock, nonce: "n:once")
|
52
|
+
|
53
|
+
assert_exception_message("version cannot be converted to an integer") {
|
54
|
+
subject.verify_proof!(proof, v1)
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_verify_fail_on_bad_v2_nonce_format
|
59
|
+
padlock = build_padlock(v1_app)
|
60
|
+
proof = build_proof(v1_app, padlock, version: 2)
|
61
|
+
|
62
|
+
assert_exception_message("nonce does not look like a timestamp") {
|
63
|
+
subject.verify_proof!(proof, v1)
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_verify_fail_on_v2_nonce_out_of_fuzz
|
68
|
+
nonce = timestamp_nonce(-11, :minutes)
|
69
|
+
padlock = build_padlock(v1_app, nonce: nonce, version: 2)
|
70
|
+
proof = build_proof(v1_app, padlock, version: 2, nonce: nonce)
|
71
|
+
|
72
|
+
assert_exception_message("nonce is invalid") {
|
73
|
+
subject.verify_proof!(proof, v1)
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_verify_fail_on_v1_nonce_for_v2_app
|
78
|
+
padlock = build_padlock(v2_app, version: 1)
|
79
|
+
proof = build_proof(v2_app, padlock, version: 1)
|
80
|
+
|
81
|
+
assert_exception_message("proof and app version mismatch") {
|
82
|
+
subject.verify_proof!(proof, v2)
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_verify_success_v1
|
87
|
+
padlock = build_padlock(v1_app)
|
88
|
+
proof = build_proof(v1_app, padlock)
|
89
|
+
|
90
|
+
assert_equal v1_app.verify, subject.verify_proof!(proof, v1)
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_verify_success_v2_default_fuzz
|
94
|
+
nonce = timestamp_nonce(-6, :minutes)
|
95
|
+
padlock = build_padlock(v2_app, nonce: nonce)
|
96
|
+
proof = build_proof(v2_app, padlock, version: 2, nonce: nonce)
|
97
|
+
|
98
|
+
assert_equal v2_app.verify, subject.verify_proof!(proof, v2)
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_verify_success_v2_custom_fuzz
|
102
|
+
nonce = timestamp_nonce(-2, :minutes)
|
103
|
+
padlock = build_padlock(v2_app(300), nonce: nonce)
|
104
|
+
proof = build_proof(v2_app, padlock, version: 2, nonce: nonce)
|
105
|
+
|
106
|
+
assert_equal v2_app.verify, subject.verify_proof!(proof, v2)
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_verify_fail_on_different_app_ids
|
110
|
+
padlock = build_padlock(v1_app, id: "00000000-0000-0000-0000-000000000000")
|
111
|
+
proof = build_proof(v1_app, padlock, id: "00000000-0000-0000-0000-000000000000")
|
112
|
+
|
113
|
+
assert_exception_message("proof and app do not match") {
|
114
|
+
subject.verify_proof!(proof, v1)
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_verify_fail_on_bad_padlock
|
119
|
+
padlock = build_padlock(v1_app, nonce: "foo")
|
120
|
+
proof = build_proof(v1_app, padlock)
|
121
|
+
|
122
|
+
assert_nil subject.verify_proof!(proof, v1)
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest_helper"
|
4
|
+
|
5
|
+
class TestAppIdentityApp < Minitest::Test
|
6
|
+
def subject
|
7
|
+
AppIdentity::App
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_fails_with_no_app_and_no_block
|
11
|
+
assert_exception_message("app cannot be created from input (missing value :id)") {
|
12
|
+
subject.new(Object.new)
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_id_validation
|
17
|
+
assert_exception_message("id must not be nil") {
|
18
|
+
subject.new({id: nil})
|
19
|
+
}
|
20
|
+
assert_exception_message("id must not be an empty string") {
|
21
|
+
subject.new({id: ""})
|
22
|
+
}
|
23
|
+
assert_exception_message("id must not contain colons") {
|
24
|
+
subject.new({id: "1:1"})
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_secret_validation
|
29
|
+
assert_exception_message("secret must not be nil") {
|
30
|
+
subject.new({id: 1, secret: nil})
|
31
|
+
}
|
32
|
+
assert_exception_message("secret must not be an empty string") {
|
33
|
+
subject.new({id: 1, secret: ""})
|
34
|
+
}
|
35
|
+
assert_exception_message("secret must be a binary string value") {
|
36
|
+
subject.new({id: 1, secret: 3})
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_version_validation
|
41
|
+
assert_exception_message("version must not be nil") {
|
42
|
+
subject.new({id: 1, secret: "a", version: nil})
|
43
|
+
}
|
44
|
+
assert_exception_message("version cannot be converted to an integer") {
|
45
|
+
subject.new({id: 1, secret: "a", version: ""})
|
46
|
+
}
|
47
|
+
assert_exception_message("version cannot be converted to an integer") {
|
48
|
+
subject.new({id: 1, secret: "a", version: "3.5"})
|
49
|
+
}
|
50
|
+
assert_exception_message("version must be a positive integer") {
|
51
|
+
subject.new({id: 1, secret: "a", version: 3.5})
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_config_validation
|
56
|
+
assert_exception_message("config must be nil or a map") {
|
57
|
+
subject.new({id: 1, secret: "a", version: 1, config: 3})
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_create_from_block
|
62
|
+
assert subject.new(-> { {id: 1, secret: "a", version: 1} }).frozen?
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest_helper"
|
4
|
+
|
5
|
+
require "app_identity/rack_middleware"
|
6
|
+
require "rack/test"
|
7
|
+
require "rack/mock"
|
8
|
+
|
9
|
+
class TestKineticApplicationRackMiddleware < Minitest::Test
|
10
|
+
include Rack::Test::Methods
|
11
|
+
|
12
|
+
HEADER = "HTTP_X_APP_IDENTITY"
|
13
|
+
|
14
|
+
attr_reader :options
|
15
|
+
|
16
|
+
def setup
|
17
|
+
@options = {headers: ["x-app-identity"], apps: [v1]}
|
18
|
+
end
|
19
|
+
|
20
|
+
def app
|
21
|
+
options = @options
|
22
|
+
@_app ||= Rack::Builder.new do
|
23
|
+
use AppIdentity::RackMiddleware, options
|
24
|
+
|
25
|
+
run ->(_) { [200, {"Content-Type" => "text/plain"}, ["OK"]] }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_empty_headers
|
30
|
+
get "/"
|
31
|
+
refute last_response.successful?
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_invalid_proof
|
35
|
+
get "/", {}, {HEADER => "invalid proof"}
|
36
|
+
refute last_response.successful?
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_invalid_app_header
|
40
|
+
get "/", {}, {HEADER => AppIdentity.generate_proof(v2)}
|
41
|
+
refute last_response.successful?
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_valid_app_header
|
45
|
+
get "/", {}, {HEADER => AppIdentity.generate_proof(v1)}
|
46
|
+
assert last_response.successful?
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_valid_app_v2_header
|
50
|
+
options[:apps] = [v2]
|
51
|
+
get "/", {}, {HEADER => AppIdentity.generate_proof(v2)}
|
52
|
+
assert last_response.successful?
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_valid_app_v3_header
|
56
|
+
options[:apps] = [v3]
|
57
|
+
get "/", {}, {HEADER => AppIdentity.generate_proof(v3)}
|
58
|
+
assert last_response.successful?
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_valid_app_v4_header
|
62
|
+
options[:apps] = [v4]
|
63
|
+
get "/", {}, {HEADER => AppIdentity.generate_proof(v4)}
|
64
|
+
assert last_response.successful?
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_valid_app_header_from_list
|
68
|
+
options[:apps] = [v1_app, v2_app]
|
69
|
+
get "/", {}, {HEADER => AppIdentity.generate_proof(v2)}
|
70
|
+
assert last_response.successful?
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_valid_app_multiple_headers
|
74
|
+
options[:apps] = [v1_app, v2_app]
|
75
|
+
options[:headers] = %w[x-app-identity app-identity]
|
76
|
+
|
77
|
+
get "/", {}, {
|
78
|
+
HEADER => AppIdentity.generate_proof(v2),
|
79
|
+
"HTTP_APP_IDENTITY" => AppIdentity.generate_proof(v1)
|
80
|
+
}
|
81
|
+
|
82
|
+
assert last_response.successful?
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_alternate_header
|
86
|
+
options[:headers] = ["identity-widget"]
|
87
|
+
get "/", {}, {"HTTP_IDENTITY_WIDGET" => AppIdentity.generate_proof(v1)}
|
88
|
+
assert last_response.successful?
|
89
|
+
end
|
90
|
+
end
|