two_factor_auth 0.1.1

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.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +634 -0
  3. data/Rakefile +34 -0
  4. data/app/assets/javascripts/two_factor_auth/application.js +13 -0
  5. data/app/assets/stylesheets/two_factor_auth/application.css +15 -0
  6. data/app/controllers/two_factor_auth/authentications_controller.rb +32 -0
  7. data/app/controllers/two_factor_auth/registrations_controller.rb +29 -0
  8. data/app/controllers/two_factor_auth/trusted_facets_controller.rb +10 -0
  9. data/app/controllers/two_factor_auth/two_factor_auth_controller.rb +19 -0
  10. data/app/helpers/two_factor_auth/application_helper.rb +21 -0
  11. data/app/helpers/two_factor_auth/authentications_helper.rb +17 -0
  12. data/app/helpers/two_factor_auth/registrations_helper.rb +17 -0
  13. data/app/models/two_factor_auth/authentication_client_data.rb +11 -0
  14. data/app/models/two_factor_auth/authentication_request.rb +30 -0
  15. data/app/models/two_factor_auth/authentication_response.rb +49 -0
  16. data/app/models/two_factor_auth/authentication_verifier.rb +68 -0
  17. data/app/models/two_factor_auth/client_data.rb +57 -0
  18. data/app/models/two_factor_auth/registration.rb +18 -0
  19. data/app/models/two_factor_auth/registration_request.rb +33 -0
  20. data/app/models/two_factor_auth/registration_response.rb +91 -0
  21. data/app/models/two_factor_auth/registration_verifier.rb +91 -0
  22. data/app/views/layouts/two_factor_auth/application.html.erb +16 -0
  23. data/app/views/two_factor_auth/authentications/new.html.erb +30 -0
  24. data/app/views/two_factor_auth/registrations/new.html.erb +26 -0
  25. data/config/routes.rb +3 -0
  26. data/lib/generators/templates/README +6 -0
  27. data/lib/generators/templates/initializer.rb +38 -0
  28. data/lib/generators/templates/migration.rb +15 -0
  29. data/lib/generators/two_factor_auth/install_generator.rb +32 -0
  30. data/lib/tasks/two_factor_auth_tasks.rake +13 -0
  31. data/lib/two_factor_auth/authentication_hook.rb +18 -0
  32. data/lib/two_factor_auth/engine.rb +5 -0
  33. data/lib/two_factor_auth/registration_hook.rb +17 -0
  34. data/lib/two_factor_auth/version.rb +3 -0
  35. data/lib/two_factor_auth.rb +155 -0
  36. data/test/controllers/two_factor_auth/authentications_controller_test.rb +70 -0
  37. data/test/controllers/two_factor_auth/registrations_controller_test.rb +57 -0
  38. data/test/controllers/two_factor_auth/trusted_facets_controller_test.rb +17 -0
  39. data/test/dummy/README.rdoc +28 -0
  40. data/test/dummy/Rakefile +6 -0
  41. data/test/dummy/app/assets/javascripts/application.js +13 -0
  42. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  43. data/test/dummy/app/controllers/application_controller.rb +5 -0
  44. data/test/dummy/app/controllers/secrets_controller.rb +3 -0
  45. data/test/dummy/app/helpers/application_helper.rb +2 -0
  46. data/test/dummy/app/models/user.rb +8 -0
  47. data/test/dummy/app/views/layouts/application.html.erb +16 -0
  48. data/test/dummy/app/views/secrets/index.html.erb +10 -0
  49. data/test/dummy/bin/bundle +3 -0
  50. data/test/dummy/bin/rails +4 -0
  51. data/test/dummy/bin/rake +4 -0
  52. data/test/dummy/config/application.rb +24 -0
  53. data/test/dummy/config/boot.rb +5 -0
  54. data/test/dummy/config/database.yml +25 -0
  55. data/test/dummy/config/environment.rb +5 -0
  56. data/test/dummy/config/environments/development.rb +37 -0
  57. data/test/dummy/config/environments/production.rb +78 -0
  58. data/test/dummy/config/environments/test.rb +39 -0
  59. data/test/dummy/config/initializers/assets.rb +8 -0
  60. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  61. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  62. data/test/dummy/config/initializers/devise.rb +259 -0
  63. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  64. data/test/dummy/config/initializers/inflections.rb +16 -0
  65. data/test/dummy/config/initializers/mime_types.rb +4 -0
  66. data/test/dummy/config/initializers/session_store.rb +3 -0
  67. data/test/dummy/config/initializers/two_factor_auth.rb +38 -0
  68. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  69. data/test/dummy/config/locales/devise.en.yml +60 -0
  70. data/test/dummy/config/locales/en.yml +23 -0
  71. data/test/dummy/config/routes.rb +5 -0
  72. data/test/dummy/config/secrets.yml +22 -0
  73. data/test/dummy/config.ru +4 -0
  74. data/test/dummy/db/development.sqlite3 +0 -0
  75. data/test/dummy/db/migrate/20141026231953_devise_create_users.rb +42 -0
  76. data/test/dummy/db/migrate/20141224135949_create_two_factor_auth_registrations.rb +15 -0
  77. data/test/dummy/db/schema.rb +50 -0
  78. data/test/dummy/db/test.sqlite3 +0 -0
  79. data/test/dummy/log/development.log +198 -0
  80. data/test/dummy/log/test.log +3490 -0
  81. data/test/dummy/public/404.html +67 -0
  82. data/test/dummy/public/422.html +67 -0
  83. data/test/dummy/public/500.html +66 -0
  84. data/test/dummy/public/favicon.ico +0 -0
  85. data/test/dummy/tmp/cache/assets/test/sprockets/13fe41fee1fe35b49d145bcc06610705 +0 -0
  86. data/test/dummy/tmp/cache/assets/test/sprockets/2f5173deea6c795b8fdde723bb4b63af +0 -0
  87. data/test/dummy/tmp/cache/assets/test/sprockets/357970feca3ac29060c1e3861e2c0953 +0 -0
  88. data/test/dummy/tmp/cache/assets/test/sprockets/cffd775d018f68ce5dba1ee0d951a994 +0 -0
  89. data/test/dummy/tmp/cache/assets/test/sprockets/d771ace226fc8215a3572e0aa35bb0d6 +0 -0
  90. data/test/dummy/tmp/cache/assets/test/sprockets/f7cbd26ba1d28d48de824f0e94586655 +0 -0
  91. data/test/helpers/two_factor_auth/authentication_helper_test.rb +54 -0
  92. data/test/helpers/two_factor_auth/registrations_helper_test.rb +34 -0
  93. data/test/integration/navigation_test.rb +10 -0
  94. data/test/lib/two_factor_auth_test.rb +169 -0
  95. data/test/models/two_factor_auth/authentication_request_test.rb +35 -0
  96. data/test/models/two_factor_auth/authentication_response_test.rb +44 -0
  97. data/test/models/two_factor_auth/authentication_verifier_test.rb +83 -0
  98. data/test/models/two_factor_auth/client_data_test.rb +79 -0
  99. data/test/models/two_factor_auth/registration_request_test.rb +29 -0
  100. data/test/models/two_factor_auth/registration_response_test.rb +87 -0
  101. data/test/models/two_factor_auth/registration_verifier_test.rb +96 -0
  102. data/test/test_helper.rb +43 -0
  103. metadata +351 -0
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,32 @@
1
+ module TwoFactorAuth
2
+ class AuthenticationsController < TwoFactorAuthController
3
+ skip_before_action :two_factor_auth_authentication
4
+
5
+ include TwoFactorAuth::AuthenticationsHelper
6
+
7
+ def new
8
+ redirect_to after_sign_in_path_for(current_user) if user_two_factor_auth_authenticated?
9
+ end
10
+
11
+ def create
12
+ keyHandle = TwoFactorAuth::websafe_base64_decode(params.fetch(:keyHandle, ''))
13
+ registration = Registration.find_by_key_handle(keyHandle)
14
+
15
+ verifier = AuthenticationVerifier.new({
16
+ registration: registration,
17
+ request: authentication_request,
18
+ client_data: ClientData.new(encoded: params[:clientData], correct_typ: 'navigator.id.getAssertion'),
19
+ response: AuthenticationResponse.new(encoded: params[:signatureData]),
20
+ })
21
+ clear_pending_challenge
22
+
23
+ if verifier.valid?
24
+ user_two_factor_auth_authenticated! verifier.counter
25
+ redirect_to after_sign_in_path_for(current_user)
26
+ else
27
+ flash[:alert] = "Unable to authenticate"
28
+ render :new, status: 406
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ module TwoFactorAuth
2
+ class RegistrationsController < TwoFactorAuthController
3
+ skip_before_action :two_factor_auth_registration
4
+ skip_before_action :two_factor_auth_authentication
5
+
6
+ include TwoFactorAuth::RegistrationsHelper
7
+
8
+ def new
9
+ end
10
+
11
+ def create
12
+ verifier = RegistrationVerifier.new({
13
+ login: current_user,
14
+ request: registration_request,
15
+ client_data: ClientData.new(encoded: params[:clientData], correct_typ: 'navigator.id.finishEnrollment'),
16
+ response: RegistrationResponse.new(encoded: params[:registrationData]),
17
+ })
18
+ clear_pending_challenge
19
+
20
+ if verifier.save
21
+ user_two_factor_auth_authenticated! 0 # don't need to auth again after registration
22
+ redirect_to after_two_factor_auth_registrations_path_for(current_user)
23
+ else
24
+ flash[:alert] = "Unable to register"
25
+ render :new, status: 406
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ module TwoFactorAuth
2
+ # Does not inherit from ApplicationController because many of those enforce
3
+ # logins on all but a whitelist of pages and the U2F spec requires this
4
+ # respond to a request with no cookies or other auth.
5
+ class TrustedFacetsController < ActionController::Base
6
+ def index
7
+ render text: TwoFactorAuth.facets.to_json, content_type: "application/fido.trusted-apps+json"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ module TwoFactorAuth
2
+ class TwoFactorAuthController < ApplicationController
3
+ include TwoFactorAuth::ApplicationHelper
4
+ include Devise::Controllers::Helpers
5
+
6
+ protect_from_forgery with: :exception
7
+ before_action :authenticate_user!
8
+
9
+ private
10
+
11
+ def after_two_factor_auth_registrations_path_for(resource)
12
+ signed_in_root_path(resource)
13
+ end
14
+
15
+ def after_two_factor_auth_authentication_path_for(resource)
16
+ after_sign_in_path_for(resource)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module TwoFactorAuth
2
+ module ApplicationHelper
3
+ def user_two_factor_auth_registered?
4
+ current_user.registrations.any?
5
+ end
6
+
7
+ def user_two_factor_auth_authenticated! counter
8
+ Registration.authenticated(current_user, counter)
9
+ user_session['two_factor_auth_authenticated'] = Time.now
10
+ end
11
+
12
+ def user_two_factor_auth_authenticated?
13
+ user_session['two_factor_auth_authenticated'].present?
14
+ end
15
+
16
+ def user_two_factor_auth_authenticated! counter
17
+ Registration.authenticated(current_user, counter)
18
+ user_session['two_factor_auth_authenticated'] = Time.now
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module TwoFactorAuth
2
+ module AuthenticationsHelper
3
+ def authentication_request
4
+ @authentication_request ||= AuthenticationRequest.new(
5
+ TwoFactorAuth.trusted_facet_list_url,
6
+ Registration.key_handle_for_authentication(current_user),
7
+ user_session['pending_authentication_request_challenge']
8
+ )
9
+ user_session['pending_authentication_request_challenge'] = @authentication_request.challenge
10
+ @authentication_request
11
+ end
12
+
13
+ def clear_pending_challenge
14
+ user_session.delete 'pending_authentication_request_challenge'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module TwoFactorAuth
2
+ module RegistrationsHelper
3
+ def registration_request
4
+ @registration_request ||= RegistrationRequest.new(
5
+ TwoFactorAuth.trusted_facet_list_url,
6
+ [],
7
+ user_session['pending_registration_request_challenge']
8
+ )
9
+ user_session['pending_registration_request_challenge'] = @registration_request.challenge
10
+ @registration_request
11
+ end
12
+
13
+ def clear_pending_challenge
14
+ user_session.delete 'pending_registration_request_challenge'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module TwoFactorAuth
2
+ class AuthenticationClientData < ClientData
3
+ validate :typ_correct
4
+
5
+ def typ_correct
6
+ if typ != 'navigator.id.getAssertion'
7
+ errors.add :typ, "should be navigator.id.getAssertion but is #{typ}"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ require 'adamantium'
2
+
3
+ module TwoFactorAuth
4
+ class AuthenticationRequest
5
+ include Adamantium
6
+
7
+ attr_reader :app_id, :key_handle
8
+
9
+ def initialize app_id, key_handle, challenge=nil
10
+ @app_id = app_id
11
+ @key_handle = key_handle
12
+ @challenge = challenge
13
+ end
14
+
15
+ def challenge
16
+ @challenge || TwoFactorAuth::random_encoded_challenge
17
+ end
18
+ memoize :challenge
19
+
20
+ # this matches te browser's u2f api
21
+ def serialized
22
+ {
23
+ appId: app_id,
24
+ keyHandle: TwoFactorAuth.websafe_base64_encode(key_handle),
25
+ challenge: challenge,
26
+ version: U2F_VERSION,
27
+ }.to_json
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_model'
2
+ require 'virtus'
3
+
4
+ # See FIDO U2F "Raw Message Formats" documentation, section 4.3 "Registration
5
+ # Response Message: Success"
6
+ module TwoFactorAuth
7
+ class AuthenticationResponse
8
+ include ActiveModel::Validations
9
+ include Virtus.model
10
+
11
+ attribute :encoded, String
12
+ attribute :raw, String
13
+
14
+ attribute :bitfield, Fixnum
15
+ attribute :user_presence, Boolean
16
+ attribute :counter, Fixnum
17
+ attribute :signature, String
18
+
19
+ validates :bitfield, :user_presence, :counter, :signature, presence: true
20
+ validate :bitfield_correct, :user_is_present
21
+
22
+ def initialize *args, &blk
23
+ super
24
+ decompose_fields
25
+ end
26
+
27
+ def decompose_fields
28
+ self.raw = TwoFactorAuth.websafe_base64_decode encoded
29
+ io = StringIO.new raw
30
+
31
+ @bitfield = io.read(1).ord
32
+ @user_presence = @bitfield.ord & 1 == 1
33
+ @counter = io.read(4).unpack("N").first
34
+ @signature = io.read
35
+ end
36
+
37
+ def bitfield_correct
38
+ if bitfield.ord != 1
39
+ errors.add :bitfield, "bits 1-7 must be set to zero"
40
+ end
41
+ end
42
+
43
+ def user_is_present
44
+ if !user_presence
45
+ errors.add :user_presence, "must be set"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,68 @@
1
+ require 'active_model'
2
+ require 'virtus'
3
+
4
+ module TwoFactorAuth
5
+ class AuthenticationVerifier
6
+ include ActiveModel::Validations
7
+ include Virtus.model
8
+
9
+ attribute :registration, Registration
10
+ attribute :request, AuthenticationRequest
11
+ attribute :client_data, ClientData
12
+ attribute :response, AuthenticationResponse
13
+
14
+ validate :client_challenge_matches, :client_origin_matches,
15
+ :counter_advances,
16
+ :verify_signature
17
+ validates_associated :client_data, :response
18
+
19
+ def application_parameter
20
+ OpenSSL::Digest::SHA256.new.digest(request.app_id.encode('ASCII-8BIT'))
21
+ end
22
+
23
+ def challenge_parameter
24
+ OpenSSL::Digest::SHA256.new.digest(client_data.json)
25
+ end
26
+
27
+ def client_challenge_matches
28
+ if client_data.challenge != request.challenge
29
+ errors.add :client_data, "challenge does not match the challenge they were sent"
30
+ end
31
+ end
32
+
33
+ def client_origin_matches
34
+ if client_data.origin != request.app_id
35
+ errors.add :client_data, "origin does not match the appId they were sent"
36
+ end
37
+ end
38
+
39
+ def counter_advances
40
+ if response.counter <= registration.counter
41
+ errors.add :response, "does not advance counter - could mean device was cloned"
42
+ end
43
+ end
44
+
45
+ def digest
46
+ data = [
47
+ application_parameter,
48
+ response.bitfield.chr,
49
+ [response.counter].pack('N'),
50
+ challenge_parameter,
51
+ ].join('')
52
+ OpenSSL::Digest::SHA256.new.digest(data)
53
+ end
54
+
55
+ def verify_signature
56
+ ec = OpenSSL::PKey::EC.new('prime256v1')
57
+ ec.public_key = TwoFactorAuth.decode_pubkey registration.public_key
58
+ return false if ec.public_key.nil?
59
+ if !ec.dsa_verify_asn1(digest, response.signature)
60
+ errors.add :response, "signature is not correct"
61
+ end
62
+ end
63
+
64
+ def counter
65
+ response.counter
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,57 @@
1
+ require 'active_model'
2
+ require 'virtus'
3
+
4
+ module TwoFactorAuth
5
+ class ClientData
6
+ include ActiveModel::Validations
7
+ include Virtus.model
8
+
9
+ attribute :correct_typ, String
10
+ attribute :encoded, String
11
+ attribute :attrs, Hash
12
+
13
+ attribute :json, String
14
+ attribute :typ, String
15
+ attribute :challenge, String
16
+ attribute :origin, String
17
+ attribute :cid_pubkey, String
18
+
19
+ validates :correct_typ, :typ, :challenge, :origin, presence: true
20
+ validates :correct_typ, inclusion: { in: %w{navigator.id.getAssertion navigator.id.finishEnrollment} }
21
+ validate :client_data_has_correct_keys, :typ_correct
22
+
23
+ def initialize *args, &blk
24
+ super
25
+ decompose_attrs if encoded.present?
26
+ raise ArgumentError, "correct_typ is mandatory" if correct_typ.blank?
27
+ end
28
+
29
+ def decompose_attrs
30
+ self.json = TwoFactorAuth::websafe_base64_decode(encoded)
31
+ self.attrs = JSON.parse(json)
32
+
33
+ self.typ = attrs['typ']
34
+ self.challenge = attrs['challenge']
35
+ self.origin = attrs['origin']
36
+ self.cid_pubkey = attrs['cid_pubkey']
37
+ rescue ArgumentError => e
38
+ errors.add(:encoded, "Can't decode base64: #{e.message}")
39
+ rescue JSON::ParserError => e
40
+ errors.add(:json, "Can't parse json: #{e.message}")
41
+ end
42
+
43
+ def client_data_has_correct_keys
44
+ if attrs.keys != %w{typ challenge origin cid_pubkey}
45
+ errors.add :attrs, "has wrong keys: #{attrs.keys.join(', ')}"
46
+ end
47
+ end
48
+
49
+ def typ_correct
50
+ if typ != correct_typ
51
+ errors.add :typ, "should be navigator.id.getAssertion but is #{typ}"
52
+ end
53
+ end
54
+
55
+ def persisted? ; false ; end
56
+ end
57
+ end
@@ -0,0 +1,18 @@
1
+ module TwoFactorAuth
2
+ class Registration < ActiveRecord::Base
3
+ belongs_to :login, polymorphic: true
4
+
5
+ validates :login, :key_handle, :public_key, :certificate, :counter, :last_authenticated_at, presence: true
6
+
7
+ def self.key_handle_for_authentication login
8
+ login.registrations.first.key_handle
9
+ end
10
+
11
+ def self.authenticated login, counter
12
+ reg = login.registrations.first
13
+ reg.last_authenticated_at = Time.now
14
+ reg.counter = counter
15
+ reg.save!
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ require 'adamantium'
2
+
3
+ module TwoFactorAuth
4
+ class RegistrationRequest
5
+ include Adamantium
6
+
7
+ attr_reader :app_id, :key_handles
8
+
9
+ def initialize app_id=TwoFactorAuth.trusted_facet_list_url, key_handles=[], challenge=nil
10
+ @app_id = app_id
11
+ @key_handles = key_handles
12
+ @challenge = challenge
13
+ end
14
+
15
+ def challenge
16
+ @challenge || TwoFactorAuth::random_encoded_challenge
17
+ end
18
+ memoize :challenge
19
+
20
+ def signs
21
+ []
22
+ end
23
+
24
+ # this matches te browser's u2f api
25
+ def serialized
26
+ {
27
+ appId: app_id,
28
+ challenge: challenge,
29
+ version: TwoFactorAuth::U2F_VERSION,
30
+ }.to_json
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,91 @@
1
+ require 'active_model'
2
+ require 'virtus'
3
+
4
+ # See FIDO U2F "Raw Message Formats" documentation, section 4.3 "Registration
5
+ # Response Message: Success"
6
+ module TwoFactorAuth
7
+ class RegistrationResponse
8
+ include ActiveModel::Validations
9
+ include Virtus.model
10
+
11
+ USER_PUBKEY_LENGTH = 65
12
+
13
+ attribute :encoded, String
14
+ attribute :raw, String
15
+
16
+ attribute :reserved_byte, Fixnum
17
+ attribute :public_key, String
18
+ attribute :key_handle, String
19
+ attribute :certificate, String
20
+ attribute :signature, String
21
+
22
+ validates :reserved_byte, :public_key, :key_handle, :certificate,
23
+ :signature, presence: true
24
+ validate :reserved_byte_correct, :certificate_valid,
25
+ :certificate_trusted, :public_key_valid
26
+
27
+ def initialize *args, &blk
28
+ super
29
+ decompose_fields if encoded.present?
30
+ end
31
+
32
+ def decompose_fields
33
+ self.raw = TwoFactorAuth::websafe_base64_decode(encoded)
34
+ io = StringIO.new raw
35
+
36
+ self.reserved_byte = io.read(1)
37
+ self.public_key = io.read(USER_PUBKEY_LENGTH)
38
+ key_handle_length = io.readbyte
39
+ self.key_handle = io.read(key_handle_length)
40
+
41
+ at_peek = io.read(4)
42
+ io.seek(-4, IO::SEEK_CUR)
43
+ attestation_certificate_length = 4 + at_peek[2..3].unpack('n').first
44
+ self.certificate = io.read(attestation_certificate_length)
45
+ self.signature = io.read
46
+ rescue ArgumentError => e
47
+ errors.add(:encoded, "Can't decode base64: #{e.message}")
48
+ rescue EOFError => e
49
+ errors.add(:raw, "Can't extract all fields")
50
+ end
51
+
52
+ def certificate_public_key
53
+ OpenSSL::X509::Certificate.new(certificate).public_key.public_key
54
+ end
55
+
56
+ def reserved_byte_correct
57
+ if reserved_byte.ord != 5
58
+ errors.add :reserved_byte, "must be 0x05"
59
+ end
60
+ end
61
+
62
+ def certificate_valid
63
+ OpenSSL::X509::Certificate.new(certificate)
64
+ rescue TypeError # from certificate_public_key not extracting cert and using nil
65
+ errors.add(:certificate, "was not extracted")
66
+ rescue OpenSSL::X509::CertificateError
67
+ errors.add :certificate, "not a valid x509 certificate"
68
+ end
69
+
70
+ def public_key_valid
71
+ if !TwoFactorAuth.pubkey_valid?(public_key)
72
+ errors.add :public_key, "not a valid public key"
73
+ end
74
+ end
75
+
76
+ # FIDO raw message formats v1.0 page 5 says "The relying party should also
77
+ # verify that the attestation certificate was issued by a trusted
78
+ # certification authority. The exact process of setting up trusted
79
+ # certification authorities is to be defined by the FIDO Alliance and is
80
+ # outside the scope of this document."
81
+ # This hasn't yet been defined and may turn out to only be a way for the
82
+ # FIDO alliance to extract money from client creators. Or it may turn out to
83
+ # be something that servers want to configure. So this is a placeholder
84
+ # method to remind that this may be important in the future.
85
+ def certificate_trusted
86
+ true
87
+ end
88
+
89
+ def persisted? ; false ; end
90
+ end
91
+ end
@@ -0,0 +1,91 @@
1
+ require 'active_model'
2
+ require 'virtus'
3
+
4
+ module TwoFactorAuth
5
+ class RegistrationVerifier
6
+ include ActiveModel::Validations
7
+ #include TwoFactorAuth::ValidateAssociated
8
+ include Virtus.model
9
+
10
+ attribute :login
11
+ attribute :request, RegistrationRequest
12
+ attribute :client_data, ClientData
13
+ attribute :response, RegistrationResponse
14
+
15
+ validates :request, :client_data, :response, presence: true
16
+ validate :client_challenge_matches,
17
+ :client_origin_matches,
18
+ :verify_signature
19
+ validates_associated :client_data, :response
20
+
21
+ def application_parameter
22
+ OpenSSL::Digest::SHA256.new.digest(request.app_id.encode('ASCII-8BIT'))
23
+ end
24
+
25
+ def challenge_parameter
26
+ OpenSSL::Digest::SHA256.new.digest(client_data.json)
27
+ end
28
+
29
+ def digest
30
+ data = [
31
+ 0.chr.encode('ASCII-8BIT'),
32
+ application_parameter,
33
+ challenge_parameter,
34
+ response.key_handle,
35
+ response.public_key,
36
+ ].join('')
37
+ OpenSSL::Digest::SHA256.new.digest(data)
38
+ end
39
+
40
+ def client_challenge_matches
41
+ if client_data.challenge != request.challenge
42
+ errors.add :client_data, "challenge does not match the challenge they were sent"
43
+ end
44
+ end
45
+
46
+ def client_origin_matches
47
+ if client_data.origin != request.app_id
48
+ errors.add :client_data, "origin does not match the appId they were sent"
49
+ end
50
+ end
51
+
52
+ def verify_signature
53
+ ec = OpenSSL::PKey::EC.new('prime256v1')
54
+ ec.public_key = response.certificate_public_key
55
+ return false if ec.public_key.nil?
56
+ if !ec.dsa_verify_asn1(digest, response.signature)
57
+ errors.add :response, "signature is not correct"
58
+ end
59
+ rescue TypeError # from certificate_public_key not extracting cert and using nil
60
+ errors.add(:response, "certificate was not extracted")
61
+ rescue OpenSSL::PKey::ECError # signature is invalid, not just incorrect
62
+ errors.add(:response, "signature not valid")
63
+ end
64
+
65
+ def persisted? ; false ; end
66
+
67
+ def save
68
+ if valid?
69
+ persist!
70
+ true
71
+ else
72
+ false
73
+ end
74
+ end
75
+
76
+ def registration_attributes
77
+ {
78
+ login: login,
79
+ counter: 0,
80
+ key_handle: response.key_handle,
81
+ public_key: response.public_key,
82
+ certificate: response.certificate,
83
+ last_authenticated_at: Time.now,
84
+ }
85
+ end
86
+
87
+ def persist!
88
+ Registration.create!(registration_attributes)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>TwoFactorAuth</title>
5
+ <%= stylesheet_link_tag "two_factor_auth/application", media: "all" %>
6
+ <%= javascript_include_tag "two_factor_auth/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <p class="notice"><%= notice %></p>
12
+ <p class="alert twofactorauth-status"><%= alert %></p>
13
+ <%= yield %>
14
+
15
+ </body>
16
+ </html>