web_authn 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0f760c97d1bf71827f8f5fc1ae2e0da5ada27e221e7ab85ae001f1f97be6210
4
- data.tar.gz: 0b4ec0fa7befdccfe91666f0f550318513222aee0600b1263c44743c1211d590
3
+ metadata.gz: fd0d84bc14a9f6773426c3c509b379268f26b36ee5555fd8c3603a45cc677221
4
+ data.tar.gz: 124bfe151246e6380a59984f95784f6f39671038ddf98aed99dd8d4b8b59233f
5
5
  SHA512:
6
- metadata.gz: 6048298d255edac0b1d0bcf5689118c11087bc514ef60a6972dcaf6316fde3c3ec09b58ebda43b49e3d95e6e736cb37dfa8998382419689eeb28c29064ee0c07
7
- data.tar.gz: 10ed1a6298c6b8a22f9840c7f967a369501bc7429ac123f2baf7e69f1155efd157eefacbd8de11bf7aff9a29b41f80b7a5b6fe81977cef2810480c789b2e1f7b
6
+ metadata.gz: 811e1addfca6f94a181bb59dc02fd1bde4ac6c5692e5a1ed5903eb8b7063db5409061ab2880e3425be6f940aac2394d90d1223dea6d4c0a1262560388ba2fa38
7
+ data.tar.gz: faf55e02e28045d0cd1a767170bf20f606a9329bd6c351f8d81ea974e6c78d0e08d4d02bd2e714431cddc3ff3fd0651106325487e8c257da3fecc2072ebcf356
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.0.1
@@ -0,0 +1,33 @@
1
+ module WebAuthn
2
+ class AttestationObject
3
+ attr_accessor :format, :attestation_statement, :authenticator_data
4
+ alias_method :fmt, :format
5
+ alias_method :att_stmt, :attestation_statement
6
+ alias_method :auth_data, :authenticator_data
7
+
8
+ %i(credential_id rp_id_hash flags public_key sign_count).each do |method|
9
+ delegate method, to: :authenticator_data
10
+ end
11
+
12
+ def initialize(attrs)
13
+ self.format = attrs[:fmt]
14
+ self.attestation_statement = case format
15
+ when 'none'
16
+ nil
17
+ when 'packed', 'tpm', 'android-key', 'android-safetynet', 'fido-u2f'
18
+ raise "Unsupported Attestation Format: #{attestation_object[:fmt]}"
19
+ else
20
+ raise 'Unknown Attestation Format'
21
+ end
22
+ self.authenticator_data = AuthenticatorData.decode attrs[:authData]
23
+ end
24
+
25
+ class << self
26
+ def decode(encoded_attestation_object)
27
+ new CBOR.decode(
28
+ Base64.urlsafe_decode64 encoded_attestation_object
29
+ ).with_indifferent_access
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ module WebAuthn
2
+ class AttestedCredentialData
3
+ attr_accessor :aaguid, :credential_id, :public_key
4
+
5
+ def initialize(aaguid:, credential_id:, public_key:)
6
+ self.aaguid = aaguid
7
+ self.credential_id = credential_id
8
+ self.public_key = public_key
9
+ end
10
+
11
+ class << self
12
+ def decode(attested_credential_data)
13
+ length = (
14
+ ((attested_credential_data.getbyte(16) << 8) & 0xFF) +
15
+ (attested_credential_data.getbyte(17) & 0xFF)
16
+ )
17
+ aaguid,
18
+ credential_id,
19
+ _encoded_cose_key_ = [
20
+ attested_credential_data.byteslice(0...16),
21
+ attested_credential_data.byteslice(18...(18 + length)),
22
+ attested_credential_data.byteslice((18 + length)..-1),
23
+ ]
24
+ cose_key = COSE::Key::EC2.from_cbor(_encoded_cose_key_)
25
+ crv = case cose_key.curve
26
+ when 1
27
+ :'P-256'
28
+ when 2
29
+ :'P-384'
30
+ when 3
31
+ :'P-521'
32
+ else
33
+ raise 'Non-supported EC curve'
34
+ end
35
+ jwk = JSON::JWK.new(
36
+ kty: :EC,
37
+ crv: crv,
38
+ x: Base64.urlsafe_encode64(cose_key.x_coordinate, padding: false),
39
+ y: Base64.urlsafe_encode64(cose_key.y_coordinate, padding: false),
40
+ )
41
+ new(
42
+ aaguid: Base64.urlsafe_encode64(aaguid, padding: false),
43
+ credential_id: Base64.urlsafe_encode64(credential_id, padding: false),
44
+ public_key: jwk.to_key
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ module WebAuthn
2
+ class AuthenticatorData
3
+ class Flags
4
+ _flags_ = [:up, :uv, :at, :ex]
5
+ attr_accessor *_flags_
6
+ _flags_.each do |flag|
7
+ alias_method :"#{flag}?", flag
8
+ end
9
+
10
+ def initialize(up:, uv:, at:, ex:)
11
+ self.up = up
12
+ self.uv = uv
13
+ self.at = at
14
+ self.ex = ex
15
+ end
16
+
17
+ class << self
18
+ def decode(input)
19
+ bit_array = input.getbyte(0)
20
+ new(
21
+ up: bit_array[0] == 1,
22
+ uv: bit_array[2] == 1,
23
+ at: bit_array[6] == 1,
24
+ ex: bit_array[7] == 1
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,4 +1,47 @@
1
1
  module WebAuthn
2
- class AuthData
2
+ class AuthenticatorData
3
+ attr_accessor :rp_id_hash, :flags, :sign_count, :attested_credential_data
4
+
5
+ %i(credential_id public_key).each do |method|
6
+ delegate method, to: :attested_credential_data, allow_nil: true
7
+ end
8
+
9
+ def initialize(rp_id_hash:, flags:, sign_count:, attested_credential_data: nil)
10
+ self.rp_id_hash = rp_id_hash
11
+ self.flags = flags
12
+ self.sign_count = sign_count
13
+ self.attested_credential_data = attested_credential_data
14
+ end
15
+
16
+ class << self
17
+ def decode(auth_data)
18
+ rp_id_hash,
19
+ _flags_,
20
+ sign_count = [
21
+ auth_data.byteslice(0...32),
22
+ auth_data.byteslice(32),
23
+ auth_data.byteslice(33...37)
24
+ ]
25
+ flags = Flags.decode(_flags_)
26
+ attested_credential_data = if flags.at?
27
+ if flags.ex?
28
+ raise 'Extension Data Not Supported Yet'
29
+ else
30
+ AttestedCredentialData.decode auth_data.byteslice(37..-1)
31
+ end
32
+ else
33
+ nil
34
+ end
35
+
36
+ new(
37
+ rp_id_hash: Base64.urlsafe_encode64(rp_id_hash, padding: false),
38
+ flags: flags,
39
+ sign_count: sign_count.unpack('N1').first,
40
+ attested_credential_data: attested_credential_data
41
+ )
42
+ end
43
+ end
3
44
  end
4
45
  end
46
+
47
+ require 'web_authn/authenticator_data/flags'
@@ -1,4 +1,23 @@
1
1
  module WebAuthn
2
2
  class ClientDataJSON
3
+ attr_accessor :type, :origin, :challenge, :raw
4
+
5
+ def initialize(attrs = {})
6
+ self.type = attrs[:type]
7
+ self.origin = attrs[:origin]
8
+ self.challenge = attrs[:challenge]
9
+ self.raw = attrs[:raw]
10
+ end
11
+
12
+ class << self
13
+ def decode(encoded_client_data_json)
14
+ raw_client_data_json = Base64.urlsafe_decode64 encoded_client_data_json
15
+ new JSON.parse(
16
+ raw_client_data_json
17
+ ).merge(
18
+ raw: raw_client_data_json
19
+ ).with_indifferent_access
20
+ end
21
+ end
3
22
  end
4
23
  end
@@ -0,0 +1,54 @@
1
+ module WebAuthn
2
+ class Context
3
+ class Authentication < Context
4
+ attr_accessor :authenticator_data
5
+
6
+ # TODO: will need more methods, or let developers access deep methods by themselves.
7
+ %i(rp_id_hash flags sign_count).each do |method|
8
+ delegate method, to: :authenticator_data
9
+ end
10
+
11
+ def authentication?
12
+ true
13
+ end
14
+
15
+ def verify!(encoded_authenticator_data, public_key:, sign_count:, signature:)
16
+ raw_authenticator_data = Base64.urlsafe_decode64 encoded_authenticator_data
17
+ self.authenticator_data = AuthenticatorData.decode(
18
+ raw_authenticator_data
19
+ )
20
+ verify_sign_count!(sign_count, authenticator_data.sign_count)
21
+ verify_signature!(raw_authenticator_data, client_data_json.raw, public_key, signature)
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ def verify_sign_count!(before, current)
28
+ if before == 0 && current == 0
29
+ self # NOTE: no counter supported on the authenticator
30
+ elsif before < current
31
+ self
32
+ else
33
+ raise 'Invalid Sign Count'
34
+ end
35
+ end
36
+
37
+ def verify_signature!(raw_authenticator_data, raw_client_data_json, public_key, signature)
38
+ signature_base_string = [
39
+ raw_authenticator_data,
40
+ OpenSSL::Digest::SHA256.digest(raw_client_data_json)
41
+ ].join
42
+ result = public_key.dsa_verify_asn1(
43
+ OpenSSL::Digest::SHA256.digest(signature_base_string),
44
+ Base64.urlsafe_decode64(signature)
45
+ )
46
+ if result
47
+ self
48
+ else
49
+ raise 'Invalid Signature'
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,23 @@
1
+ module WebAuthn
2
+ class Context
3
+ class Registration < Context
4
+ attr_accessor :attestation_object
5
+
6
+ # TODO: will need more methods, or let developers access deep methods by themselves.
7
+ %i(credential_id rp_id_hash flags public_key sign_count).each do |method|
8
+ delegate method, to: :attestation_object
9
+ end
10
+
11
+ def registration?
12
+ true
13
+ end
14
+
15
+ def verify!(encoded_attestation_object)
16
+ self.attestation_object = AttestationObject.decode(
17
+ encoded_attestation_object
18
+ )
19
+ self
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ module WebAuthn
2
+ class Context
3
+ attr_accessor :client_data_json
4
+
5
+ def initialize(client_data_json)
6
+ self.client_data_json = client_data_json
7
+ end
8
+
9
+ def verify_session!(origin:, challenge:)
10
+ raise 'Invalid Client Data JSON Origin' unless client_data_json.origin == origin
11
+ raise 'Invalid Client Data JSON Session' unless client_data_json.challenge == challenge
12
+ self
13
+ end
14
+
15
+ def registration?
16
+ false
17
+ end
18
+
19
+ def authentication?
20
+ false
21
+ end
22
+
23
+ class << self
24
+ def for(encoded_client_data_json, origin:, challenge:)
25
+ client_data_json = ClientDataJSON.decode encoded_client_data_json
26
+
27
+ context = case client_data_json.type
28
+ when 'webauthn.create'
29
+ Registration.new(client_data_json)
30
+ when 'webauthn.get'
31
+ Authentication.new(client_data_json)
32
+ else
33
+ raise 'Unknown Client Data JSON Type'
34
+ end
35
+
36
+ context.verify_session!(origin: origin, challenge: challenge)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ require 'web_authn/context/authentication'
43
+ require 'web_authn/context/registration'
data/lib/web_authn.rb CHANGED
@@ -1,7 +1,24 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'cbor'
4
+ require 'cose'
5
+ require 'cose/key/ec2'
6
+ require 'json/jwt'
7
+
1
8
  module WebAuthn
2
- # TODO:
9
+ module_function
10
+
11
+ def context_for(encoded_client_data_json, origin:, challenge:)
12
+ Context.for(
13
+ encoded_client_data_json,
14
+ origin: origin,
15
+ challenge: challenge
16
+ )
17
+ end
3
18
  end
4
19
 
5
20
  require 'web_authn/attestation_object'
21
+ require 'web_authn/attested_credential_data'
6
22
  require 'web_authn/authenticator_data'
7
23
  require 'web_authn/client_data_json'
24
+ require 'web_authn/context'
@@ -0,0 +1,46 @@
1
+ require 'web_authn'
2
+
3
+ authenticator_data = 'MsuA3KzDw1JGLLAfO_4wLebzcS8w_SDs0Zw7pbhYlJUBAAAAOw'
4
+
5
+ signature = 'MEUCIQDXp8Wqzz3ZYV7avKvH3R3XQhW7xPYb5Cq2nx3gpflDGwIgPN0tSy2mmgpI06IIKmjrIUxCvL4Rfc53mFXfVd_yL58'
6
+ sign_count = 0
7
+
8
+ client_data_json = 'eyJjaGFsbGVuZ2UiOiJjbUZ1Wkc5dExYTjBjbWx1WnkxblpXNWxjbUYwWldRdFlua3RjbkF0YzJWeWRtVnkiLCJvcmlnaW4iOiJodHRwczovL3dlYi1hdXRobi5zZWxmLWlzc3VlZC5hcHAiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0'
9
+
10
+ origin = 'https://web-authn.self-issued.app'
11
+ challenge = 'cmFuZG9tLXN0cmluZy1nZW5lcmF0ZWQtYnktcnAtc2VydmVy'
12
+
13
+ public_key = OpenSSL::PKey::EC.new <<-PEM
14
+ -----BEGIN PUBLIC KEY-----
15
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMpNU/8TjYoyN8FlhZ+YsOMAvyfQ4
16
+ i6/JN0/DPXuZMoxLvdb1vjh7vPUt2Osw3Bq+0NZsx3U/8kmpFuwsZhTi9A==
17
+ -----END PUBLIC KEY-----
18
+ PEM
19
+
20
+ context = WebAuthn.context_for(
21
+ client_data_json,
22
+ origin: origin,
23
+ challenge: challenge,
24
+ )
25
+ raise unless context.authentication?
26
+
27
+ context.verify!(
28
+ authenticator_data,
29
+ public_key: public_key,
30
+ sign_count: sign_count,
31
+ signature: signature
32
+ )
33
+
34
+ puts <<-OUT
35
+ # RP ID Hash
36
+ #{context.rp_id_hash}
37
+
38
+ # Flags
39
+ up: #{context.flags.up}
40
+ uv: #{context.flags.uv}
41
+ at: #{context.flags.at}
42
+ ex: #{context.flags.ex}
43
+
44
+ # Sign Count
45
+ #{context.sign_count}
46
+ OUT
@@ -0,0 +1,34 @@
1
+ # Common
2
+ context = WebAuthn.context_for(
3
+ client_data_json,
4
+ origin: request.base_url,
5
+ challenge: session[:challenge],
6
+ )
7
+
8
+ # Registration
9
+ raise unless context.registration?
10
+
11
+ context.verify!(params[:attestation_object])
12
+ current_account.fido_authenticators.create(
13
+ credential_id: context.credential_id,
14
+ public_key: context.public_key.to_pem,
15
+ sign_count: context.sign_count
16
+ )
17
+
18
+ # Authentication
19
+ raise unless context.authentication?
20
+
21
+ fido_authentiator = FIDO::Authenticatior.find_by(credential_id: params[:credential_id])
22
+ raise unless fido_authentiator.present?
23
+
24
+ context.verify!(
25
+ authenticator_data,
26
+ public_key: fido_authentiator.public_key,
27
+ sign_count: fido_authentiator.sign_count,
28
+ signature: params[:signature]
29
+ )
30
+
31
+ fido_authentiator.update!(
32
+ sign_count: context.sign_count
33
+ )
34
+ authenticate authenticator.user
@@ -0,0 +1,37 @@
1
+ require 'web_authn'
2
+
3
+ attestation_object = 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEMsuA3KzDw1JGLLAfO_4wLebzcS8w_SDs0Zw7pbhYlJVBAAAAMAAAAAAAAAAAAAAAAAAAAAAAQM1zXqvmYeVH9o2q1YcBZDSlkhvVs_2RjnKESVUktkQwQnYcU8jdo-duNLKrIOZNg0g4RCm0UMDZxtdXhR2bCu2lAQIDJiABIVggDMGhDLXoZit2uSMLyL-_emlFrGzlH7b2KpKpgYNzPRYiWCAl795OxcS2QimEnC9Jl_pNG3Gy_9O6m3_GbZdGsk90aw'
4
+
5
+ client_data_json = 'eyJjaGFsbGVuZ2UiOiJjbUZ1Wkc5dExYTjBjbWx1WnkxblpXNWxjbUYwWldRdFlua3RjbkF0YzJWeWRtVnkiLCJuZXdfa2V5c19tYXlfYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCIsIm9yaWdpbiI6Imh0dHBzOi8vd2ViLWF1dGhuLnNlbGYtaXNzdWVkLmFwcCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ'
6
+
7
+ origin = 'https://web-authn.self-issued.app'
8
+ challenge = 'cmFuZG9tLXN0cmluZy1nZW5lcmF0ZWQtYnktcnAtc2VydmVy'
9
+
10
+ context = WebAuthn.context_for(
11
+ client_data_json,
12
+ origin: origin,
13
+ challenge: challenge
14
+ )
15
+ raise unless context.registration?
16
+
17
+ context.verify! attestation_object
18
+
19
+ puts <<-OUT
20
+ # RP ID Hash
21
+ #{context.rp_id_hash}
22
+
23
+ # Flags
24
+ up: #{context.flags.up}
25
+ uv: #{context.flags.uv}
26
+ at: #{context.flags.at}
27
+ ex: #{context.flags.ex}
28
+
29
+ # Credential ID
30
+ #{context.credential_id}
31
+
32
+ # Public Key
33
+ #{context.public_key.to_pem}
34
+
35
+ # Sign Count
36
+ #{context.sign_count}
37
+ OUT
data/web_authn.gemspec CHANGED
@@ -12,8 +12,10 @@ Gem::Specification.new do |gem|
12
12
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
13
13
  gem.require_paths = ['lib']
14
14
  gem.required_ruby_version = '>= 2.3'
15
- gem.add_runtime_dependency 'json-jwt', '>= 1.9.4'
16
- gem.add_runtime_dependency 'cbor', '>= 0.5.9.3'
15
+ gem.add_runtime_dependency 'activesupport'
16
+ gem.add_runtime_dependency 'cbor'
17
+ gem.add_runtime_dependency 'cose'
18
+ gem.add_runtime_dependency 'json-jwt'
17
19
  gem.add_development_dependency 'rake', '~> 10.0'
18
20
  gem.add_development_dependency 'simplecov'
19
21
  gem.add_development_dependency 'rspec'
metadata CHANGED
@@ -1,43 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web_authn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - nov matake
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-29 00:00:00.000000000 Z
11
+ date: 2018-09-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: json-jwt
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.9.4
19
+ version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 1.9.4
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: cbor
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.5.9.3
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: cose
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json-jwt
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
34
62
  type: :runtime
35
63
  prerelease: false
36
64
  version_requirements: !ruby/object:Gem::Requirement
37
65
  requirements:
38
66
  - - ">="
39
67
  - !ruby/object:Gem::Version
40
- version: 0.5.9.3
68
+ version: '0'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: rake
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -97,9 +125,7 @@ dependencies:
97
125
  description: W3C WebAuthn (a.k.a. FIDO2) RP library in Ruby
98
126
  email:
99
127
  - nov@matake.jp
100
- executables:
101
- - console
102
- - setup
128
+ executables: []
103
129
  extensions: []
104
130
  extra_rdoc_files: []
105
131
  files:
@@ -111,12 +137,18 @@ files:
111
137
  - README.md
112
138
  - Rakefile
113
139
  - VERSION
114
- - bin/console
115
- - bin/setup
116
140
  - lib/web_authn.rb
117
141
  - lib/web_authn/attestation_object.rb
142
+ - lib/web_authn/attested_credential_data.rb
118
143
  - lib/web_authn/authenticator_data.rb
144
+ - lib/web_authn/authenticator_data/flags.rb
119
145
  - lib/web_authn/client_data_json.rb
146
+ - lib/web_authn/context.rb
147
+ - lib/web_authn/context/authentication.rb
148
+ - lib/web_authn/context/registration.rb
149
+ - samples/authentication_response.rb
150
+ - samples/concept.rb
151
+ - samples/registration_response.rb
120
152
  - spec/spec_helper.rb
121
153
  - spec/web_authn_spec.rb
122
154
  - web_authn.gemspec
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "web_authn"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here