├── .rspec ├── .ruby-version ├── example ├── public │ ├── javascripts │ │ ├── application.js │ │ └── u2f-api.js │ ├── stylesheets │ │ ├── application.css │ │ └── normalize.css │ └── favicon.ico ├── README.md ├── .gitignore ├── .components ├── Rakefile ├── config │ ├── database.rb │ ├── apps.rb │ └── boot.rb ├── app │ ├── helpers │ │ └── helpers.rb │ ├── app.rb │ ├── views │ │ ├── index.haml │ │ ├── authentications │ │ │ ├── show.haml │ │ │ └── new.haml │ │ ├── layouts │ │ │ └── application.html.erb │ │ └── registrations │ │ │ └── new.haml │ └── controllers │ │ ├── registrations.rb │ │ └── authentications.rb ├── config.ru ├── models │ └── registration.rb ├── bin │ └── u2f_example ├── Gemfile ├── db │ └── migrate │ │ └── 001_create_registrations.rb └── Gemfile.lock ├── lib ├── u2f │ ├── version.rb │ ├── request_base.rb │ ├── sign_request.rb │ ├── register_request.rb │ ├── registration.rb │ ├── client_data.rb │ ├── errors.rb │ ├── sign_response.rb │ ├── register_response.rb │ ├── u2f.rb │ └── fake_u2f.rb └── u2f.rb ├── .coditsu └── ci.yml ├── Rakefile ├── Gemfile ├── CHANGELOG.md ├── .rubocop.yml ├── spec ├── spec_helper.rb └── lib │ ├── register_request_spec.rb │ ├── sign_request_spec.rb │ ├── client_data_spec.rb │ ├── sign_response_spec.rb │ ├── register_response_spec.rb │ └── u2f_spec.rb ├── u2f.gemspec ├── .gitignore ├── .travis.yml ├── LICENSE ├── Gemfile.lock └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.5 2 | -------------------------------------------------------------------------------- /example/public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/public/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/castle/ruby-u2f/HEAD/example/public/favicon.ico -------------------------------------------------------------------------------- /lib/u2f/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | VERSION = '1.0.0' 5 | end 6 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | bundle install 3 | padrino rake db:migrate 4 | thin start --ssl-disable-verify 5 | ``` 6 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | log/**/* 3 | tmp/**/* 4 | vendor/gems/* 5 | !vendor/gems/cache/ 6 | .sass-cache/* 7 | db/*.db 8 | .*.sw* 9 | -------------------------------------------------------------------------------- /.coditsu/ci.yml: -------------------------------------------------------------------------------- 1 | repository_id: 'f789891f-3f4e-42be-a25b-068d49708ddb' 2 | api_key: <%= ENV['CODITSU_API_KEY'] %> 3 | api_secret: <%= ENV['CODITSU_API_SECRET'] %> 4 | -------------------------------------------------------------------------------- /example/.components: -------------------------------------------------------------------------------- 1 | --- 2 | :orm: datamapper 3 | :test: none 4 | :mock: none 5 | :script: none 6 | :renderer: haml 7 | :stylesheet: none 8 | :namespace: U2FExample 9 | :migration_format: number 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new 7 | 8 | task default: :spec 9 | task test: :spec 10 | -------------------------------------------------------------------------------- /example/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'padrino-core/cli/rake' 5 | 6 | PadrinoTasks.use(:database) 7 | PadrinoTasks.use(:datamapper) 8 | PadrinoTasks.init 9 | -------------------------------------------------------------------------------- /example/config/database.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | DataMapper.logger = logger 4 | DataMapper::Property::String.length(255) 5 | DataMapper.setup(:default, 'sqlite3://' + Padrino.root('db', 'u2f_example.db')) 6 | -------------------------------------------------------------------------------- /example/app/helpers/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | U2FExample::App.helpers do 4 | def u2f 5 | # use base_url as app_id, e.g. 'http://localhost:3000' 6 | @u2f ||= U2F::U2F.new(request.base_url) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | gemspec 5 | 6 | gem 'rake' 7 | 8 | group :test do 9 | gem 'coveralls_reborn' 10 | gem 'json_expressions' 11 | gem 'rspec' 12 | gem 'simplecov' 13 | end 14 | -------------------------------------------------------------------------------- /example/app/app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2FExample 4 | class App < Padrino::Application 5 | register Padrino::Helpers 6 | enable :sessions 7 | 8 | get '/' do 9 | render 'index' 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rackup 2 | # frozen_string_literal: true 3 | 4 | # This file can be used to start Padrino, 5 | # just execute it from the command line. 6 | 7 | require File.expand_path('config/boot.rb', __dir__) 8 | 9 | run Padrino.application 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## master 2 | - [#58](https://github.com/castle/ruby-u2f/pull/58) drop support for ruby 2.3 3 | - [#41](https://github.com/castle/ruby-u2f/pull/41) drop support for ruby 2.2 4 | 5 | ## 1.0.0 (2016-02-18) 6 | - update ruby-u2f to work with the v1.1 U2F JavaScript API. 7 | -------------------------------------------------------------------------------- /lib/u2f/request_base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | module RequestBase 5 | def to_json(options = {}) 6 | ::JSON.pretty_generate(as_json, options) 7 | end 8 | 9 | def version 10 | 'U2F_V2' 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Documentation: 2 | Enabled: false 3 | AllCops: 4 | TargetRubyVersion: 2.6 5 | 6 | Metrics/LineLength: 7 | Max: 100 8 | 9 | Metrics/ModuleLength: 10 | Exclude: 11 | - "**/*_spec.rb" 12 | 13 | Metrics/BlockLength: 14 | Exclude: 15 | - "**/*_spec.rb" 16 | -------------------------------------------------------------------------------- /example/models/registration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Registration 4 | include DataMapper::Resource 5 | 6 | # property , 7 | property :id, Serial 8 | property :key_handle, String 9 | property :public_key, String 10 | property :certificate, Text 11 | property :counter, Integer 12 | end 13 | -------------------------------------------------------------------------------- /example/config/apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Padrino.configure_apps do 4 | set :session_secret, '723c1901f2645a2f8c1bacb955f9da81346bf5a8200a4498' 5 | set :protection, except: :path_traversal 6 | set :protect_from_csrf, true 7 | end 8 | 9 | Padrino.mount('U2FExample::App', app_file: Padrino.root('app/app.rb')).to('/') 10 | -------------------------------------------------------------------------------- /example/bin/u2f_example: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | Dir.chdir(File.dirname(__FILE__) + '/..') 5 | 6 | require 'rubygems' 7 | require 'bundler/setup' 8 | require 'padrino-core/cli/launcher' 9 | 10 | ARGV.unshift('start') if ARGV.first.nil? || ARGV.first.start_with?('-') 11 | Padrino::Cli::Launcher.start ARGV 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | require 'coveralls' 5 | 6 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( 7 | [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter] 8 | ) 9 | SimpleCov.start do 10 | add_filter 'spec' 11 | end 12 | 13 | require 'json_expressions/rspec' 14 | require 'u2f' 15 | -------------------------------------------------------------------------------- /lib/u2f/sign_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | class SignRequest 5 | include RequestBase 6 | attr_accessor :key_handle 7 | 8 | def initialize(key_handle) 9 | @key_handle = key_handle 10 | end 11 | 12 | def as_json(_options = {}) 13 | { 14 | version: version, 15 | keyHandle: key_handle 16 | } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/u2f/register_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | class RegisterRequest 5 | include RequestBase 6 | attr_accessor :challenge 7 | 8 | def initialize(challenge) 9 | @challenge = challenge 10 | end 11 | 12 | def as_json(_options = {}) 13 | { 14 | version: version, 15 | challenge: challenge 16 | } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /example/app/views/index.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-4.col-md-offset-4.page-header 3 | %h2 U2F demo server 4 | .row 5 | .col-md-4.col-md-offset-4 6 | %p.lead 7 | Welcome to the demo implementation of a U2F validation server in Ruby/Padrino. 8 | %a.btn.btn-default.btn-block.btn-lg{:href => 'registrations/new'} Register a new key 9 | %a.btn.btn-default.btn-block.btn-lg{:href => 'authentications/new'} Validate a registered key 10 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Project requirements 6 | gem 'rake' 7 | 8 | # Component requirements 9 | gem 'dm-aggregates' 10 | gem 'dm-constraints' 11 | gem 'dm-core' 12 | gem 'dm-migrations' 13 | gem 'dm-sqlite-adapter' 14 | gem 'dm-timestamps' 15 | gem 'dm-types' 16 | gem 'dm-validations' 17 | gem 'haml' 18 | 19 | # Padrino Stable Gem 20 | gem 'padrino' 21 | 22 | # To enable https 23 | gem 'thin' 24 | 25 | gem 'u2f', '1.0.0' 26 | -------------------------------------------------------------------------------- /lib/u2f.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'base64' 4 | require 'json' 5 | require 'openssl' 6 | require 'securerandom' 7 | 8 | require 'u2f/client_data' 9 | require 'u2f/errors' 10 | require 'u2f/request_base' 11 | require 'u2f/register_request' 12 | require 'u2f/register_response' 13 | require 'u2f/registration' 14 | require 'u2f/sign_request' 15 | require 'u2f/sign_response' 16 | require 'u2f/fake_u2f' 17 | require 'u2f/u2f' 18 | 19 | module U2F 20 | DIGEST = OpenSSL::Digest::SHA256 21 | end 22 | -------------------------------------------------------------------------------- /example/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Defines our constants 4 | RACK_ENV = ENV['RACK_ENV'] ||= 'development' unless defined?(RACK_ENV) 5 | PADRINO_ROOT = File.expand_path('..', __dir__) unless defined?(PADRINO_ROOT) 6 | 7 | # Load our dependencies 8 | require 'rubygems' unless defined?(Gem) 9 | require 'bundler/setup' 10 | Bundler.require(:default, RACK_ENV) 11 | 12 | ## 13 | # Add your after (RE)load hooks here 14 | # 15 | Padrino.after_load do 16 | DataMapper.finalize 17 | end 18 | 19 | Padrino.load! 20 | -------------------------------------------------------------------------------- /lib/u2f/registration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | ## 5 | # A representation of a registered U2F device 6 | class Registration 7 | attr_writer :counter 8 | attr_accessor :key_handle, :public_key, :certificate 9 | 10 | def initialize(key_handle, public_key, certificate) 11 | @key_handle = key_handle 12 | @public_key = public_key 13 | @certificate = certificate 14 | end 15 | 16 | def counter 17 | @counter.nil? ? 0 : @counter 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/lib/register_request_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe U2F::RegisterRequest do 6 | let(:challenge) { 'fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g' } 7 | 8 | let(:sign_request) do 9 | described_class.new(challenge) 10 | end 11 | 12 | describe '#to_json' do 13 | subject { sign_request.to_json } 14 | 15 | it do 16 | is_expected.to match_json_expression( 17 | version: String, 18 | challenge: String 19 | ) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /example/app/views/authentications/show.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-4.col-md-offset-4.page-header.text-center 3 | %h2= @error_message ? 'Failed' : 'Success' 4 | .row 5 | .col-md-4.col-md-offset-4.text-center 6 | %p.lead 7 | %span.glyphicon{style: 'font-size: 64px;', class: @error_message ? 'text-danger glyphicon-remove' : 'text-success glyphicon-ok'} 8 | %p= @error_message ? @error_message : 'Successfully authenticated' 9 | .row 10 | .col-md-4.col-md-offset-4.text-center 11 | %hr 12 | %p 13 | %a{:href => '/'} « Back to main page 14 | -------------------------------------------------------------------------------- /example/db/migrate/001_create_registrations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | migration 1, :create_registrations do 4 | up do 5 | create_table :registrations do 6 | column :id, Integer, serial: true 7 | column :key_handle, DataMapper::Property::String, length: 255 8 | column :public_key, DataMapper::Property::String, length: 255 9 | column :certificate, DataMapper::Property::Text 10 | column :counter, DataMapper::Property::Integer 11 | end 12 | end 13 | 14 | down do 15 | drop_table :registrations 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/lib/sign_request_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe U2F::SignRequest do 6 | let(:key_handle) do 7 | 'CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w==' 8 | end 9 | let(:sign_request) do 10 | described_class.new(key_handle) 11 | end 12 | 13 | describe '#to_json' do 14 | subject { sign_request.to_json } 15 | 16 | it do 17 | is_expected.to match_json_expression( 18 | version: String, 19 | keyHandle: String 20 | ) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /u2f.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.push File.expand_path('lib', __dir__) 4 | 5 | require 'u2f/version' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = 'u2f' 9 | s.version = U2F::VERSION 10 | s.summary = 'U2F library' 11 | s.description = 'Library for handling registration and authentication of U2F devices' 12 | s.authors = ['Johan Brissmyr', 'Sebastian Wallin'] 13 | s.email = ['brissmyr@gmail.com', 'sebastian.wallin@gmail.com'] 14 | s.homepage = 'https://github.com/castle/ruby-u2f' 15 | s.license = 'MIT' 16 | s.required_ruby_version = '>= 2.4' 17 | 18 | s.files = Dir['{lib}/**/*'] + ['README.md', 'LICENSE'] 19 | s.test_files = Dir['spec/**/*'] 20 | end 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Ruby ### 2 | *.gem 3 | *.rbc 4 | /.config 5 | /coverage/ 6 | /InstalledFiles 7 | /pkg/ 8 | /spec/reports/ 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | ## Documentation cache and generated files: 14 | /.yardoc/ 15 | /_yardoc/ 16 | /doc/ 17 | /rdoc/ 18 | 19 | ## Environment normalisation: 20 | /.bundle/ 21 | /lib/bundler/man/ 22 | 23 | # for a library or gem, you might want to ignore these files since the code is 24 | # intended to run in multiple environments; otherwise, check them in: 25 | # Gemfile.lock 26 | # .ruby-version 27 | # .ruby-gemset 28 | 29 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 30 | .rvmrc 31 | 32 | # Example files 33 | /example/.bundle 34 | vendor 35 | .coditsu/local.yml 36 | -------------------------------------------------------------------------------- /lib/u2f/client_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | ## 5 | # A representation of ClientData, chapter 7 6 | # http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf 7 | class ClientData 8 | REGISTRATION_TYP = 'navigator.id.finishEnrollment' 9 | AUTHENTICATION_TYP = 'navigator.id.getAssertion' 10 | 11 | attr_accessor :typ, :challenge, :origin 12 | alias type typ 13 | 14 | def registration? 15 | typ == REGISTRATION_TYP 16 | end 17 | 18 | def authentication? 19 | typ == AUTHENTICATION_TYP 20 | end 21 | 22 | def self.load_from_json(json) 23 | client_data = ::JSON.parse(json) 24 | new.tap do |instance| 25 | instance.typ = client_data['typ'] 26 | instance.challenge = client_data['challenge'] 27 | instance.origin = client_data['origin'] 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - docker 3 | 4 | dist: trusty 5 | sudo: false 6 | cache: bundler 7 | 8 | git: 9 | depth: false 10 | 11 | test: &test 12 | stage: test 13 | language: ruby 14 | before_install: 15 | - gem install bundler 16 | - gem update --system 17 | 18 | jobs: 19 | include: 20 | - <<: *test 21 | rvm: 2.6.5 22 | - <<: *test 23 | rvm: 2.5.7 24 | - <<: *test 25 | rvm: 2.4.8 26 | 27 | - stage: coditsu 28 | language: ruby 29 | rvm: 2.6.3 30 | before_install: 31 | - gem update --system 32 | - gem install bundler 33 | before_script: 34 | - docker create -v /sources --name sources alpine:3.4 /bin/true 35 | - docker cp ./ sources:/sources 36 | script: > 37 | docker run 38 | -e CODITSU_API_KEY 39 | -e CODITSU_API_SECRET 40 | -e CODITSU_REPOSITORY_ID 41 | -e CODITSU_BUILD_BRANCH=$TRAVIS_BRANCH 42 | --volumes-from sources 43 | coditsu/build-runner:latest 44 | 45 | stages: 46 | - test 47 | - coditsu 48 | -------------------------------------------------------------------------------- /example/app/controllers/registrations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | U2FExample::App.controllers :registrations do 4 | get :new do 5 | @registration_requests = u2f.registration_requests 6 | session[:challenges] = @registration_requests.map(&:challenge) 7 | 8 | key_handles = Registration.map(&:key_handle) 9 | @sign_requests = u2f.authentication_requests(key_handles) 10 | 11 | @app_id = u2f.app_id 12 | 13 | render 'registrations/new' 14 | end 15 | 16 | post :index do 17 | response = U2F::RegisterResponse.load_from_json(params[:response]) 18 | 19 | begin 20 | reg = u2f.register!(session[:challenges], response) 21 | 22 | Registration.create!( 23 | certificate: reg.certificate, 24 | key_handle: reg.key_handle, 25 | public_key: reg.public_key, 26 | counter: reg.counter 27 | ) 28 | rescue U2F::Error => e 29 | @error_message = "Unable to register: #{e.class.name}" 30 | ensure 31 | session.delete(:challenges) 32 | end 33 | 34 | render 'authentications/show' 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 by Johan Brissmyr and Sebastian Wallin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/lib/client_data_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper.rb' 4 | 5 | describe U2F::ClientData do 6 | let(:type) { '' } 7 | let(:registration_type) { U2F::ClientData::REGISTRATION_TYP } 8 | let(:authentication_type) { U2F::ClientData::AUTHENTICATION_TYP } 9 | 10 | let(:client_data) do 11 | described_class.new.tap do |cd| 12 | cd.typ = type 13 | end 14 | end 15 | 16 | describe '#registration?' do 17 | subject { client_data.registration? } 18 | 19 | context 'with correct type' do 20 | let(:type) { registration_type } 21 | 22 | it { is_expected.to be_truthy } 23 | end 24 | 25 | context 'with incorrect type' do 26 | let(:type) { authentication_type } 27 | 28 | it { is_expected.to be_falsey } 29 | end 30 | end 31 | 32 | describe '#authentication?' do 33 | subject { client_data.authentication? } 34 | 35 | context 'with correct type' do 36 | let(:type) { authentication_type } 37 | 38 | it { is_expected.to be_truthy } 39 | end 40 | 41 | context 'with incorrect type' do 42 | let(:type) { registration_type } 43 | 44 | it { is_expected.to be_falsey } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/u2f/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | class Error < StandardError; end 5 | class UnmatchedChallengeError < Error; end 6 | class ClientDataTypeError < Error; end 7 | class PublicKeyDecodeError < Error; end 8 | class AttestationDecodeError < Error; end 9 | class AttestationVerificationError < Error; end 10 | class AttestationSignatureError < Error; end 11 | class NoMatchingRequestError < Error; end 12 | class NoMatchingRegistrationError < Error; end 13 | class CounterTooLowError < Error; end 14 | class AuthenticationFailedError < Error; end 15 | class UserNotPresentError < Error; end 16 | 17 | # This error represents various potential errors that a user can come across 18 | # while attempting to register. 19 | class RegistrationError < Error 20 | CODES = { 21 | 1 => 'OTHER_ERROR', 22 | 2 => 'BAD_REQUEST', 23 | 3 => 'CONFIGURATION_UNSUPPORTED', 24 | 4 => 'DEVICE_INELIGIBLE', 25 | 5 => 'TIMEOUT' 26 | }.freeze 27 | 28 | attr_reader :code 29 | 30 | def initialize(options = {}) 31 | @code = options[:code] 32 | message = options[:message] || "Token returned #{CODES[code]}" 33 | super(message) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /example/app/controllers/authentications.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'base64' 4 | U2FExample::App.controllers :authentications do 5 | get :new do 6 | key_handles = Registration.map(&:key_handle) 7 | return 'Need to register first' if key_handles.empty? 8 | 9 | @app_id = u2f.app_id 10 | @sign_requests = u2f.authentication_requests(key_handles) 11 | @challenge = u2f.challenge 12 | session[:u2f_challenge] = @challenge 13 | 14 | render 'authentications/new' 15 | end 16 | 17 | post :index do 18 | response = U2F::SignResponse.load_from_json(params[:response]) 19 | 20 | registration = Registration.first(key_handle: response.key_handle) 21 | return 'Need to register first' unless registration 22 | 23 | begin 24 | u2f.authenticate!(session[:u2f_challenge], response, 25 | Base64.decode64(registration.public_key), 26 | registration.counter) 27 | rescue U2F::Error => e 28 | @error_message = "Unable to authenticate: #{e.class.name}" 29 | ensure 30 | session.delete(:u2f_challenge) 31 | end 32 | 33 | registration.update(counter: response.counter) 34 | 35 | render 'authentications/show' 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | u2f (1.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | coveralls_reborn (0.16.0) 10 | simplecov (~> 0.18.1) 11 | term-ansicolor (~> 1.6) 12 | thor (>= 0.20.3, < 2.0) 13 | tins (~> 1.16) 14 | diff-lcs (1.3) 15 | docile (1.3.2) 16 | json_expressions (0.9.0) 17 | rake (13.0.1) 18 | rspec (3.9.0) 19 | rspec-core (~> 3.9.0) 20 | rspec-expectations (~> 3.9.0) 21 | rspec-mocks (~> 3.9.0) 22 | rspec-core (3.9.2) 23 | rspec-support (~> 3.9.3) 24 | rspec-expectations (3.9.2) 25 | diff-lcs (>= 1.2.0, < 2.0) 26 | rspec-support (~> 3.9.0) 27 | rspec-mocks (3.9.1) 28 | diff-lcs (>= 1.2.0, < 2.0) 29 | rspec-support (~> 3.9.0) 30 | rspec-support (3.9.3) 31 | simplecov (0.18.5) 32 | docile (~> 1.1) 33 | simplecov-html (~> 0.11) 34 | simplecov-html (0.12.2) 35 | sync (0.5.0) 36 | term-ansicolor (1.7.1) 37 | tins (~> 1.0) 38 | thor (1.0.1) 39 | tins (1.25.0) 40 | sync 41 | 42 | PLATFORMS 43 | ruby 44 | 45 | DEPENDENCIES 46 | coveralls_reborn 47 | json_expressions 48 | rake 49 | rspec 50 | simplecov 51 | u2f! 52 | 53 | BUNDLED WITH 54 | 2.1.4 55 | -------------------------------------------------------------------------------- /example/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Ruby U2F Example 10 | 11 | 12 | 13 | <%= stylesheet_link_tag '//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css' %> 14 | 15 | 18 | <%= javascript_include_tag 'application' %> 19 | <%= javascript_include_tag 'u2f-api' %> 20 | 21 | 22 | 25 |
26 | <%= yield %> 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /spec/lib/sign_response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper.rb' 4 | 5 | describe U2F::SignResponse do 6 | let(:app_id) { 'http://demo.example.com' } 7 | let(:challenge) { U2F.urlsafe_encode64(SecureRandom.random_bytes(32)) } 8 | let(:device) { U2F::FakeU2F.new(app_id) } 9 | let(:json_response) { device.sign_response(challenge) } 10 | let(:sign_response) { described_class.load_from_json json_response } 11 | let(:public_key_pem) { U2F::U2F.public_key_pem(device.origin_public_key_raw) } 12 | 13 | context 'with invalid response' do 14 | let(:json_response) { '{}' } 15 | 16 | it do 17 | expect { sign_response }.to raise_error(U2F::Error) do |error| 18 | expect(error.message).to eq('Missing required data') 19 | end 20 | end 21 | end 22 | 23 | describe '#counter' do 24 | subject { sign_response.counter } 25 | 26 | it { is_expected.to be device.counter } 27 | end 28 | 29 | describe '#user_present?' do 30 | subject { sign_response.user_present? } 31 | 32 | it { is_expected.to be true } 33 | end 34 | 35 | describe '#verify with correct app id' do 36 | subject { sign_response.verify(app_id, public_key_pem) } 37 | 38 | it { is_expected.to be_truthy } 39 | end 40 | 41 | describe '#verify with wrong app id' do 42 | subject { sign_response.verify('other app', public_key_pem) } 43 | 44 | it { is_expected.to be_falsey } 45 | end 46 | 47 | describe '#verify with corrupted signature' do 48 | subject { sign_response.verify(app_id, public_key_pem) } 49 | 50 | before { allow(sign_response).to receive(:signature).and_return('bad signature') } 51 | 52 | it { is_expected.to be_falsey } 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /example/app/views/authentications/new.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-4.col-md-offset-4.page-header 3 | %h2 Validate key 4 | .row 5 | .col-md-4.col-md-offset-4 6 | %p.lead 7 | Please insert a registered key and press the button within 15 seconds 8 | %p#waiting.text-center.text-success.well 9 | Waiting... 10 | %p#error.alert.alert-danger{style: 'display: none;'} 11 | .row 12 | .col-md-4.col-md-offset-4 13 | %p 14 | %a{:href => '/'} « Back to main page 15 | 16 | 17 | = form_tag '/authentications', method: 'post' do 18 | = hidden_field_tag :response 19 | 20 | :javascript 21 | var signRequests = #{@sign_requests.to_json.html_safe}; 22 | var challenge = #{@challenge.to_json.html_safe}; 23 | var appId = #{@app_id.to_json.html_safe}; 24 | var $waiting = document.getElementById('waiting'); 25 | var $error = document.getElementById('error'); 26 | var errorMap = { 27 | 1: 'Unknown error, try again', 28 | 2: "Bad request error, try again" , 29 | 3: "This key isn't supported, please try another one", 30 | 4: 'The device is not registered, please register first', 31 | 5: 'Authentication timed out. Please reload to try again.' 32 | }; 33 | var setError = function(code) { 34 | $waiting.style.display = 'none'; 35 | $error.style.display = 'block'; 36 | $error.innerHTML = errorMap[code]; 37 | }; 38 | 39 | u2f.sign(appId, challenge, signRequests, function(signResponse) { 40 | var form, reg; 41 | 42 | if (signResponse.errorCode) { 43 | return setError(signResponse.errorCode); 44 | } 45 | 46 | form = document.forms[0]; 47 | response = document.querySelector('[name=response]'); 48 | 49 | response.value = JSON.stringify(signResponse); 50 | 51 | form.submit(); 52 | }, 15); 53 | -------------------------------------------------------------------------------- /example/app/views/registrations/new.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-4.col-md-offset-4.page-header 3 | %h2 Register key 4 | .row 5 | .col-md-4.col-md-offset-4 6 | %p.lead 7 | Please insert the key and press the button within 15 seconds 8 | %p#waiting.text-center.text-success.well 9 | Waiting... 10 | %p#error.alert.alert-danger{style: 'display: none;'} 11 | .row 12 | .col-md-4.col-md-offset-4 13 | %p 14 | %a{:href => '/'} « Back to main page 15 | 16 | = form_tag '/registrations', method: 'post' do 17 | = hidden_field_tag :response 18 | 19 | :javascript 20 | var appId = #{@app_id.to_json.html_safe}; 21 | var registerRequests = #{@registration_requests.to_json.html_safe}; 22 | var signRequests = #{@sign_requests.to_json.html_safe}; 23 | var $waiting = document.getElementById('waiting'); 24 | var $error = document.getElementById('error'); 25 | var errorMap = { 26 | 1: 'Unknown error, try again', 27 | 2: "Bad request error, try again" , 28 | 3: "This key isn't supported, please try another one", 29 | 4: 'The device is already registered, please login', 30 | 5: 'Authentication timed out. Please reload to try again.' 31 | }; 32 | var setError = function(code) { 33 | $waiting.style.display = 'none'; 34 | $error.style.display = 'block'; 35 | $error.innerHTML = errorMap[code]; 36 | }; 37 | 38 | u2f.register(appId, registerRequests, signRequests, function(registerResponse) { 39 | var form, reg; 40 | 41 | if (registerResponse.errorCode) { 42 | return setError(registerResponse.errorCode); 43 | } 44 | 45 | form = document.forms[0]; 46 | response = document.querySelector('[name=response]'); 47 | 48 | response.value = JSON.stringify(registerResponse); 49 | 50 | form.submit(); 51 | }, 15); 52 | -------------------------------------------------------------------------------- /lib/u2f/sign_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | class SignResponse 5 | attr_accessor :client_data, :client_data_json, :key_handle, :signature_data 6 | 7 | def self.load_from_json(json) 8 | data = ::JSON.parse(json) 9 | if !data.key?('clientData') || !data.key?('keyHandle') || 10 | !data.key?('signatureData') 11 | raise Error, 'Missing required data' 12 | end 13 | 14 | instance = new 15 | instance.client_data_json = 16 | ::U2F.urlsafe_decode64(data['clientData']) 17 | instance.client_data = 18 | ClientData.load_from_json(instance.client_data_json) 19 | instance.key_handle = data['keyHandle'] 20 | instance.signature_data = 21 | ::U2F.urlsafe_decode64(data['signatureData']) 22 | instance 23 | end 24 | 25 | ## 26 | # Counter value that the U2F token increments every time it performs an 27 | # authentication operation 28 | def counter 29 | signature_data.byteslice(1, 4).unpack('N').first 30 | end 31 | 32 | ## 33 | # signature is to be verified using the public key obtained during 34 | # registration. 35 | def signature 36 | signature_data.byteslice(5..-1) 37 | end 38 | 39 | # Bit 0 being set to 1 indicates that the user is present. A different value 40 | # of Bit 0, as well as Bits 1 through 7, are reserved for future use. 41 | USER_PRESENCE_MASK = 0b00000001 42 | 43 | ## 44 | # If user presence was verified 45 | def user_present? 46 | byte = signature_data.byteslice(0).unpack('C').first 47 | byte & USER_PRESENCE_MASK == 1 48 | end 49 | 50 | ## 51 | # Verifies the response against an app id and the public key of the 52 | # registered device 53 | def verify(app_id, public_key_pem) 54 | data = [ 55 | ::U2F::DIGEST.digest(app_id), 56 | signature_data.byteslice(0, 5), 57 | ::U2F::DIGEST.digest(client_data_json) 58 | ].join 59 | 60 | public_key = OpenSSL::PKey.read(public_key_pem) 61 | 62 | begin 63 | public_key.verify(::U2F::DIGEST.new, signature, data) 64 | rescue OpenSSL::PKey::PKeyError 65 | false 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/lib/register_response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper.rb' 4 | 5 | describe U2F::RegisterResponse do 6 | let(:app_id) { 'http://demo.example.com' } 7 | let(:challenge) { U2F.urlsafe_encode64(SecureRandom.random_bytes(32)) } 8 | let(:device) { U2F::FakeU2F.new(app_id) } 9 | let(:key_handle) { U2F.urlsafe_encode64(device.key_handle_raw) } 10 | let(:public_key) { Base64.strict_encode64(device.origin_public_key_raw) } 11 | let(:certificate) { Base64.strict_encode64(device.cert_raw) } 12 | let(:registration_data_json) { device.register_response(challenge) } 13 | let(:registration_data_json_without_padding) do 14 | device.register_response(challenge).delete(' ') 15 | end 16 | let(:error_response) { device.register_response(challenge, true) } 17 | let(:registration_request) { U2F::RegisterRequest.new(challenge) } 18 | let(:register_response) do 19 | described_class.load_from_json(registration_data_json) 20 | end 21 | 22 | context 'with error response' do 23 | let(:registration_data_json) { error_response } 24 | 25 | it 'raises RegistrationError with code' do 26 | expect { register_response }.to raise_error(U2F::RegistrationError) do |error| 27 | expect(error.code).to eq(4) 28 | end 29 | end 30 | end 31 | 32 | context 'with invalid response' do 33 | let(:registration_data_json) { '{}' } 34 | 35 | it 'raises RegistrationError with code' do 36 | expect { register_response }.to raise_error(U2F::RegistrationError) do |error| 37 | expect(error.message).to eq('Invalid JSON') 38 | end 39 | end 40 | end 41 | 42 | context 'with unpadded response' do 43 | let(:registration_data_json) { registration_data_json_without_padding } 44 | 45 | it 'does not raise "invalid base64" exception' do 46 | expect { register_response }.not_to raise_error 47 | end 48 | end 49 | 50 | describe '#certificate' do 51 | subject { register_response.certificate } 52 | 53 | it { is_expected.to eq certificate } 54 | end 55 | 56 | describe '#client_data' do 57 | context 'when challenge' do 58 | subject { register_response.client_data.challenge } 59 | 60 | it { is_expected.to eq challenge } 61 | end 62 | end 63 | 64 | describe '#key_handle' do 65 | subject { register_response.key_handle } 66 | 67 | it { is_expected.to eq key_handle } 68 | end 69 | 70 | describe '#key_handle_length' do 71 | subject { register_response.key_handle_length } 72 | 73 | it { is_expected.to eq U2F.urlsafe_decode64(key_handle).length } 74 | end 75 | 76 | describe '#public_key' do 77 | subject { register_response.public_key } 78 | 79 | it { is_expected.to eq public_key } 80 | end 81 | 82 | describe '#verify' do 83 | subject { register_response.verify(app_id) } 84 | 85 | it { is_expected.to be_truthy } 86 | end 87 | 88 | describe '#verify with wrong app_id' do 89 | subject { register_response.verify('other app') } 90 | 91 | it { is_expected.to be_falsey } 92 | end 93 | 94 | describe '#verify with corrupted signature' do 95 | subject { register_response } 96 | 97 | it 'returns falsey' do 98 | allow(subject).to receive(:signature).and_return('bad signature') 99 | expect(subject.verify(app_id)).to be_falsey 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/u2f/register_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | ## 5 | # Representation of a U2F registration response. 6 | # See chapter 4.3: 7 | # http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf 8 | class RegisterResponse 9 | attr_accessor :client_data, :client_data_json, :registration_data_raw 10 | 11 | PUBLIC_KEY_OFFSET = 1 12 | PUBLIC_KEY_LENGTH = 65 13 | KEY_HANDLE_LENGTH_LENGTH = 1 14 | KEY_HANDLE_LENGTH_OFFSET = PUBLIC_KEY_OFFSET + PUBLIC_KEY_LENGTH 15 | KEY_HANDLE_OFFSET = KEY_HANDLE_LENGTH_OFFSET + KEY_HANDLE_LENGTH_LENGTH 16 | 17 | def self.load_from_json(json) 18 | # TODO: validate 19 | data = JSON.parse(json) 20 | 21 | raise RegistrationError, code: data['errorCode'] if data['errorCode']&.positive? 22 | 23 | if !data.key?('clientData') || !data.key?('registrationData') 24 | raise RegistrationError, message: 'Invalid JSON' 25 | end 26 | 27 | new.tap do |instance| 28 | instance.client_data_json = 29 | ::U2F.urlsafe_decode64(data['clientData']) 30 | instance.client_data = 31 | ClientData.load_from_json(instance.client_data_json) 32 | instance.registration_data_raw = 33 | ::U2F.urlsafe_decode64(data['registrationData']) 34 | end 35 | end 36 | 37 | ## 38 | # The attestation certificate in Base64 encoded X.509 DER format 39 | def certificate 40 | Base64.strict_encode64(parsed_certificate.to_der) 41 | end 42 | 43 | ## 44 | # The parsed attestation certificate 45 | def parsed_certificate 46 | OpenSSL::X509::Certificate.new(certificate_bytes) 47 | end 48 | 49 | ## 50 | # Length of the attestation certificate 51 | def certificate_length 52 | parsed_certificate.to_der.bytesize 53 | end 54 | 55 | ## 56 | # Returns the key handle from registration data, URL safe base64 encoded 57 | def key_handle 58 | ::U2F.urlsafe_encode64(key_handle_raw) 59 | end 60 | 61 | def key_handle_raw 62 | registration_data_raw.byteslice(KEY_HANDLE_OFFSET, key_handle_length) 63 | end 64 | 65 | ## 66 | # Returns the length of the key handle, extracted from the registration data 67 | def key_handle_length 68 | registration_data_raw.byteslice(KEY_HANDLE_LENGTH_OFFSET).unpack('C').first 69 | end 70 | 71 | ## 72 | # Returns the public key, extracted from the registration data 73 | def public_key 74 | # Base64 encode without line feeds 75 | Base64.strict_encode64(public_key_raw) 76 | end 77 | 78 | def public_key_raw 79 | registration_data_raw.byteslice(PUBLIC_KEY_OFFSET, PUBLIC_KEY_LENGTH) 80 | end 81 | 82 | ## 83 | # Returns the signature, extracted from the registration data 84 | def signature 85 | registration_data_raw.byteslice( 86 | (KEY_HANDLE_OFFSET + key_handle_length + certificate_length)..-1 87 | ) 88 | end 89 | 90 | ## 91 | # Verifies the registration data against the app id 92 | def verify(app_id) 93 | # Chapter 4.3 in 94 | # http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf 95 | data = [ 96 | "\x00", 97 | ::U2F::DIGEST.digest(app_id), 98 | ::U2F::DIGEST.digest(client_data_json), 99 | key_handle_raw, 100 | public_key_raw 101 | ].join 102 | 103 | begin 104 | parsed_certificate.public_key.verify(::U2F::DIGEST.new, signature, data) 105 | rescue OpenSSL::PKey::PKeyError 106 | false 107 | end 108 | end 109 | 110 | private 111 | 112 | def certificate_bytes 113 | base_offset = KEY_HANDLE_OFFSET + key_handle_length 114 | registration_data_raw.byteslice(base_offset..-1) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.1) 5 | public_suffix (>= 2.0.2, < 6.0) 6 | bcrypt (3.1.18) 7 | bcrypt-ruby (3.1.5) 8 | bcrypt (>= 3.1.3) 9 | concurrent-ruby (1.2.2) 10 | daemons (1.2.6) 11 | data_objects (0.10.17) 12 | addressable (~> 2.1) 13 | date (3.4.1) 14 | dm-aggregates (1.2.0) 15 | dm-core (~> 1.2.0) 16 | dm-constraints (1.2.0) 17 | dm-core (~> 1.2.0) 18 | dm-core (1.2.1) 19 | addressable (~> 2.3) 20 | dm-do-adapter (1.2.0) 21 | data_objects (~> 0.10.6) 22 | dm-core (~> 1.2.0) 23 | dm-migrations (1.2.0) 24 | dm-core (~> 1.2.0) 25 | dm-sqlite-adapter (1.2.0) 26 | dm-do-adapter (~> 1.2.0) 27 | do_sqlite3 (~> 0.10.6) 28 | dm-timestamps (1.2.0) 29 | dm-core (~> 1.2.0) 30 | dm-types (1.2.2) 31 | bcrypt-ruby (~> 3.0) 32 | dm-core (~> 1.2.0) 33 | fastercsv (~> 1.5) 34 | json (~> 1.6) 35 | multi_json (~> 1.0) 36 | stringex (~> 1.4) 37 | uuidtools (~> 2.1) 38 | dm-validations (1.2.0) 39 | dm-core (~> 1.2.0) 40 | do_sqlite3 (0.10.17) 41 | data_objects (= 0.10.17) 42 | eventmachine (1.2.7) 43 | fastercsv (1.5.5) 44 | haml (5.0.4) 45 | temple (>= 0.8.0) 46 | tilt 47 | i18n (0.9.5) 48 | concurrent-ruby (~> 1.0) 49 | json (1.8.6) 50 | mail (2.8.1) 51 | mini_mime (>= 0.1.1) 52 | net-imap 53 | net-pop 54 | net-smtp 55 | mime-types (2.99.3) 56 | mini_mime (1.1.2) 57 | moneta (1.1.1) 58 | multi_json (1.15.0) 59 | mustermann (2.0.2) 60 | ruby2_keywords (~> 0.0.1) 61 | net-imap (0.3.9) 62 | date 63 | net-protocol 64 | net-pop (0.1.2) 65 | net-protocol 66 | net-protocol (0.2.2) 67 | timeout 68 | net-smtp (0.3.3) 69 | net-protocol 70 | padrino (0.15.3) 71 | padrino-admin (= 0.15.3) 72 | padrino-cache (= 0.15.3) 73 | padrino-core (= 0.15.3) 74 | padrino-gen (= 0.15.3) 75 | padrino-helpers (= 0.15.3) 76 | padrino-mailer (= 0.15.3) 77 | padrino-support (= 0.15.3) 78 | padrino-admin (0.15.3) 79 | padrino-core (= 0.15.3) 80 | padrino-helpers (= 0.15.3) 81 | padrino-cache (0.15.3) 82 | moneta (~> 1.1.0) 83 | padrino-core (= 0.15.3) 84 | padrino-helpers (= 0.15.3) 85 | padrino-core (0.15.3) 86 | padrino-support (= 0.15.3) 87 | sinatra (>= 2.2.4) 88 | thor (~> 1.0) 89 | padrino-gen (0.15.3) 90 | bundler (>= 1.0, < 3) 91 | padrino-core (= 0.15.3) 92 | padrino-helpers (0.15.3) 93 | i18n (>= 0.6.7, < 2) 94 | padrino-support (= 0.15.3) 95 | tilt (>= 1.4.1, < 3) 96 | padrino-mailer (0.15.3) 97 | mail (~> 2.5) 98 | mime-types (< 4) 99 | padrino-core (= 0.15.3) 100 | padrino-support (0.15.3) 101 | public_suffix (5.0.1) 102 | rack (2.2.20) 103 | rack-protection (2.2.4) 104 | rack 105 | rake (12.3.3) 106 | ruby2_keywords (0.0.5) 107 | sinatra (2.2.4) 108 | mustermann (~> 2.0) 109 | rack (~> 2.2) 110 | rack-protection (= 2.2.4) 111 | tilt (~> 2.0) 112 | stringex (1.5.1) 113 | temple (0.8.0) 114 | thin (1.7.2) 115 | daemons (~> 1.0, >= 1.0.9) 116 | eventmachine (~> 1.0, >= 1.0.4) 117 | rack (>= 1, < 3) 118 | thor (1.2.1) 119 | tilt (2.1.0) 120 | timeout (0.4.3) 121 | u2f (1.0.0) 122 | uuidtools (2.2.0) 123 | 124 | PLATFORMS 125 | ruby 126 | 127 | DEPENDENCIES 128 | dm-aggregates 129 | dm-constraints 130 | dm-core 131 | dm-migrations 132 | dm-sqlite-adapter 133 | dm-timestamps 134 | dm-types 135 | dm-validations 136 | haml 137 | padrino 138 | rake 139 | thin 140 | u2f (= 1.0.0) 141 | 142 | BUNDLED WITH 143 | 2.6.8 144 | -------------------------------------------------------------------------------- /spec/lib/u2f_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe U2F do 6 | let(:app_id) { 'http://demo.example.com' } 7 | let(:device_challenge) { described_class.urlsafe_encode64(SecureRandom.random_bytes(32)) } 8 | let(:auth_challenge) { device_challenge } 9 | let(:u2f) { described_class::U2F.new(app_id) } 10 | let(:device) { described_class::FakeU2F.new(app_id) } 11 | let(:key_handle) { described_class.urlsafe_encode64(device.key_handle_raw) } 12 | let(:certificate) { Base64.strict_encode64(device.cert_raw) } 13 | let(:public_key) { device.origin_public_key_raw } 14 | let(:register_response_json) { device.register_response(device_challenge) } 15 | let(:sign_response_json) { device.sign_response(device_challenge) } 16 | let(:registration) do 17 | described_class::Registration.new(key_handle, public_key, certificate) 18 | end 19 | let(:register_response) do 20 | described_class::RegisterResponse.load_from_json(register_response_json) 21 | end 22 | let(:sign_response) do 23 | described_class::SignResponse.load_from_json sign_response_json 24 | end 25 | let(:sign_request) do 26 | described_class::SignRequest.new(key_handle) 27 | end 28 | 29 | describe '#authentication_requests' do 30 | let(:requests) { u2f.authentication_requests(key_handle) } 31 | 32 | it 'returns an array of requests' do 33 | expect(requests).to be_an Array 34 | requests.each { |r| expect(r).to be_a described_class::SignRequest } 35 | end 36 | end 37 | 38 | describe '#authenticate!' do 39 | let(:counter) { registration.counter } 40 | let(:reg_public_key) { registration.public_key } 41 | let(:u2f_authenticate) do 42 | u2f.authenticate!(auth_challenge, sign_response, reg_public_key, counter) 43 | end 44 | 45 | context 'with correct parameters' do 46 | it 'does not raise an error' do 47 | expect { u2f_authenticate }.not_to raise_error 48 | end 49 | end 50 | 51 | context 'with incorrect challenge' do 52 | let(:auth_challenge) { 'incorrect' } 53 | 54 | it 'raises NoMatchingRequestError' do 55 | expect { u2f_authenticate }.to raise_error(described_class::NoMatchingRequestError) 56 | end 57 | end 58 | 59 | context 'with incorrect counter' do 60 | let(:counter) { 1000 } 61 | 62 | it 'raises CounterTooLowError' do 63 | expect { u2f_authenticate }.to raise_error(described_class::CounterTooLowError) 64 | end 65 | end 66 | 67 | context 'with incorrect counter' do 68 | let(:reg_public_key) { "\x00" } 69 | 70 | it 'raises CounterToLowError' do 71 | expect { u2f_authenticate }.to raise_error(described_class::PublicKeyDecodeError) 72 | end 73 | end 74 | end 75 | 76 | describe '#registration_requests' do 77 | let(:requests) { u2f.registration_requests } 78 | 79 | it 'returns an array of requests' do 80 | expect(requests).to be_an Array 81 | requests.each { |r| expect(r).to be_a described_class::RegisterRequest } 82 | end 83 | end 84 | 85 | describe '#register!' do 86 | context 'with correct registration data' do 87 | it 'returns a registration' do 88 | reg = nil 89 | expect do 90 | reg = u2f.register!(auth_challenge, register_response) 91 | end.not_to raise_error 92 | expect(reg.key_handle).to eq key_handle 93 | end 94 | 95 | it 'accepts an array of challenges' do 96 | reg = u2f.register!(['another-challenge', auth_challenge], register_response) 97 | expect(reg).to be_a described_class::Registration 98 | end 99 | end 100 | 101 | context 'with unknown challenge' do 102 | let(:auth_challenge) { 'non-matching' } 103 | 104 | it 'raises an UnmatchedChallengeError' do 105 | expect do 106 | u2f.register!(auth_challenge, register_response) 107 | end.to raise_error(described_class::UnmatchedChallengeError) 108 | end 109 | end 110 | end 111 | 112 | describe '::public_key_pem' do 113 | context 'with correct key' do 114 | it 'wraps the result' do 115 | pem = described_class::U2F.public_key_pem public_key 116 | expect(pem).to start_with '-----BEGIN PUBLIC KEY-----' 117 | expect(pem).to end_with '-----END PUBLIC KEY-----' 118 | end 119 | end 120 | 121 | context 'with invalid key' do 122 | let(:public_key) do 123 | described_class.urlsafe_decode64( 124 | 'YV6FVSmH0ObY1cBRCsYJZ/CXF1gKsL+DW46rMfpeymtDZted2Ut2BraszUK1wg1+YJ4Bxt6r24WHNUYqKgeaSq8=' 125 | ) 126 | end 127 | 128 | it 'fails when first byte of the key is not 0x04' do 129 | expect do 130 | described_class::U2F.public_key_pem public_key 131 | end.to raise_error(described_class::PublicKeyDecodeError) 132 | end 133 | end 134 | 135 | context 'with truncated key' do 136 | let(:public_key) { described_class.urlsafe_decode64('BJhSPkR3Rmgl') } 137 | 138 | it 'fails when key is to short' do 139 | expect do 140 | described_class::U2F.public_key_pem public_key 141 | end.to raise_error(described_class::PublicKeyDecodeError) 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/u2f/u2f.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | class U2F 5 | attr_accessor :app_id 6 | ## 7 | # * *Args*: 8 | # - +app_id+:: An application (facet) ID string 9 | # 10 | def initialize(app_id) 11 | @app_id = app_id 12 | end 13 | 14 | ## 15 | # Generate data to be sent to the U2F device before authenticating 16 | # 17 | # * *Args*: 18 | # - +key_handles+:: +Array+ of previously registered U2F key handles 19 | # 20 | # * *Returns*: 21 | # - An +Array+ of +SignRequest+ objects 22 | # 23 | def authentication_requests(key_handles) 24 | key_handles = [key_handles] unless key_handles.is_a? Array 25 | key_handles.map do |key_handle| 26 | SignRequest.new(key_handle) 27 | end 28 | end 29 | 30 | ## 31 | # Authenticate a response from the U2F device 32 | # 33 | # * *Args*: 34 | # - +challenge+:: Challenge string 35 | # - +response+:: Response from the U2F device as a +SignResponse+ object 36 | # - +registration_public_key+:: Public key of the registered U2F device as binary string 37 | # - +registration_counter+:: +Integer+ with the current counter value of the registered 38 | # device 39 | # 40 | # * *Raises*: 41 | # - +NoMatchingRequestError+:: if the challenge in the response doesn't match any of the 42 | # provided ones 43 | # - +ClientDataTypeError+:: if the response is of the wrong type 44 | # - +AuthenticationFailedError+:: if the authentication failed 45 | # - +UserNotPresentError+:: if the user wasn't present during the authentication 46 | # - +CounterTooLowError+:: if there is a counter mismatch between the registered one and 47 | # the one in the response 48 | # 49 | def authenticate!(challenge, response, registration_public_key, registration_counter) 50 | # TODO: check that it's the correct key_handle as well 51 | raise NoMatchingRequestError unless challenge == response.client_data.challenge 52 | 53 | raise ClientDataTypeError unless response.client_data.authentication? 54 | 55 | pem = U2F.public_key_pem(registration_public_key) 56 | 57 | raise AuthenticationFailedError unless response.verify(app_id, pem) 58 | 59 | raise UserNotPresentError unless response.user_present? 60 | 61 | unless response.counter > registration_counter 62 | raise CounterTooLowError unless response.counter.zero? && registration_counter.zero? 63 | end 64 | end 65 | 66 | ## 67 | # Generates a 32 byte long random U2F challenge 68 | # 69 | # * *Returns*: 70 | # - Base64 urlsafe encoded challenge 71 | # 72 | def challenge 73 | ::U2F.urlsafe_encode64(SecureRandom.random_bytes(32)) 74 | end 75 | 76 | ## 77 | # Generate data to be used when registering a U2F device 78 | # 79 | # * *Returns*: 80 | # - An +Array+ of +RegisterRequest+ objects 81 | # 82 | def registration_requests 83 | # TODO: generate a request for each supported version 84 | [RegisterRequest.new(challenge)] 85 | end 86 | 87 | ## 88 | # Authenticate the response from the U2F device when registering 89 | # 90 | # * *Args*: 91 | # - +challenges+:: +Array+ of challenge strings 92 | # - +response+:: Response of the U2F device as a +RegisterResponse+ object 93 | # 94 | # * *Returns*: 95 | # - A +Registration+ object 96 | # 97 | # * *Raises*: 98 | # - +UnmatchedChallengeError+:: if the challenge in the response doesn't match any of 99 | # the provided ones 100 | # - +ClientDataTypeError+:: if the response is of the wrong type 101 | # - +AttestationSignatureError+:: if the registration failed 102 | # 103 | def register!(challenges, response) 104 | challenges = [challenges] unless challenges.is_a? Array 105 | challenge = challenges.detect do |chg| 106 | chg == response.client_data.challenge 107 | end 108 | 109 | raise UnmatchedChallengeError unless challenge 110 | 111 | raise ClientDataTypeError unless response.client_data.registration? 112 | 113 | # Validate public key 114 | U2F.public_key_pem(response.public_key_raw) 115 | 116 | raise AttestationSignatureError unless response.verify(app_id) 117 | 118 | Registration.new( 119 | response.key_handle, 120 | response.public_key, 121 | response.certificate 122 | ) 123 | end 124 | 125 | ## 126 | # Convert a binary public key to PEM format 127 | # * *Args*: 128 | # - +key+:: Binary public key 129 | # 130 | # * *Returns*: 131 | # - A base64 encoded public key +String+ in PEM format 132 | # 133 | # * *Raises*: 134 | # - +PublicKeyDecodeError+:: if the +key+ argument is incorrect 135 | # 136 | def self.public_key_pem(key) 137 | raise PublicKeyDecodeError unless key.bytesize == 65 && key.byteslice(0) == "\x04" 138 | 139 | # http://tools.ietf.org/html/rfc5480 140 | der = OpenSSL::ASN1::Sequence( 141 | [ 142 | OpenSSL::ASN1::Sequence( 143 | [ 144 | OpenSSL::ASN1::ObjectId('1.2.840.10045.2.1'), # id-ecPublicKey 145 | OpenSSL::ASN1::ObjectId('1.2.840.10045.3.1.7') # secp256r1 146 | ] 147 | ), 148 | OpenSSL::ASN1::BitString(key) 149 | ] 150 | ).to_der 151 | 152 | pem = "-----BEGIN PUBLIC KEY-----\r\n" + 153 | Base64.strict_encode64(der).scan(/.{1,64}/).join("\r\n") + 154 | "\r\n-----END PUBLIC KEY-----" 155 | pem 156 | end 157 | end 158 | 159 | ## 160 | # Variant of Base64::urlsafe_decode64 which adds padding if necessary 161 | # 162 | def self.urlsafe_decode64(string) 163 | string = case string.length % 4 164 | when 2 then string + '==' 165 | when 3 then string + '=' 166 | else string 167 | end 168 | Base64.urlsafe_decode64(string) 169 | end 170 | 171 | ## 172 | # Variant of Base64::urlsafe_encode64 which removes padding 173 | # 174 | def self.urlsafe_encode64(string) 175 | Base64.urlsafe_encode64(string).delete('=') 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /lib/u2f/fake_u2f.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module U2F 4 | # This class is for mocking a U2F device for testing purposes. 5 | class FakeU2F 6 | CURVE_NAME = 'prime256v1' 7 | 8 | attr_accessor :app_id, :counter, :key_handle_raw, :cert_subject 9 | 10 | # Initialize a new FakeU2F device for use in tests. 11 | # 12 | # app_id - The appId/origin this is being tested against. 13 | # options - A Hash of optional parameters (optional). 14 | # :counter - The initial counter for this device. 15 | # :key_handle - The raw key-handle this device should use. 16 | # :cert_subject - The subject field for the certificate generated 17 | # for this device. 18 | # 19 | # Returns nothing. 20 | def initialize(app_id, options = {}) 21 | @app_id = app_id 22 | @counter = options.fetch(:counter, 0) 23 | @key_handle_raw = options.fetch(:key_handle, SecureRandom.random_bytes(32)) 24 | @cert_subject = options.fetch(:cert_subject, '/CN=U2FTest') 25 | end 26 | 27 | # A registerResponse hash as returned by the u2f.register JavaScript API. 28 | # 29 | # challenge - The challenge to sign. 30 | # error - Boolean. Whether to return an error response (optional). 31 | # 32 | # Returns a JSON encoded Hash String. 33 | def register_response(challenge, error = false) 34 | if error 35 | JSON.dump(errorCode: 4) 36 | else 37 | client_data_json = client_data(::U2F::ClientData::REGISTRATION_TYP, challenge) 38 | JSON.dump( 39 | registrationData: reg_registration_data(client_data_json), 40 | clientData: ::U2F.urlsafe_encode64(client_data_json) 41 | ) 42 | end 43 | end 44 | 45 | # A SignResponse hash as returned by the u2f.sign JavaScript API. 46 | # 47 | # challenge - The challenge to sign. 48 | # 49 | # Returns a JSON encoded Hash String. 50 | def sign_response(challenge) 51 | client_data_json = client_data(::U2F::ClientData::AUTHENTICATION_TYP, challenge) 52 | JSON.dump( 53 | clientData: ::U2F.urlsafe_encode64(client_data_json), 54 | keyHandle: ::U2F.urlsafe_encode64(key_handle_raw), 55 | signatureData: auth_signature_data(client_data_json) 56 | ) 57 | end 58 | 59 | # The appId specific public key as returned in the registrationData field of 60 | # a RegisterResponse Hash. 61 | # 62 | # Returns a binary formatted EC public key String. 63 | def origin_public_key_raw 64 | [origin_key.public_key.to_bn.to_s(16)].pack('H*') 65 | end 66 | 67 | # The raw device attestation certificate as returned in the registrationData 68 | # field of a RegisterResponse Hash. 69 | # 70 | # Returns a DER formatted certificate String. 71 | def cert_raw 72 | cert.to_der 73 | end 74 | 75 | private 76 | 77 | # The registrationData field returns in a RegisterResponse Hash. 78 | # 79 | # client_data_json - The JSON encoded clientData String. 80 | # 81 | # Returns a url-safe base64 encoded binary String. 82 | def reg_registration_data(client_data_json) 83 | ::U2F.urlsafe_encode64( 84 | [ 85 | 5, 86 | origin_public_key_raw, 87 | key_handle_raw.bytesize, 88 | key_handle_raw, 89 | cert_raw, 90 | reg_signature(client_data_json) 91 | ].pack("CA65CA#{key_handle_raw.bytesize}A#{cert_raw.bytesize}A*") 92 | ) 93 | end 94 | 95 | # The signature field of a registrationData field of a RegisterResponse. 96 | # 97 | # client_data_json - The JSON encoded clientData String. 98 | # 99 | # Returns an ECDSA signature String. 100 | def reg_signature(client_data_json) 101 | payload = [ 102 | "\x00", 103 | ::U2F::DIGEST.digest(app_id), 104 | ::U2F::DIGEST.digest(client_data_json), 105 | key_handle_raw, 106 | origin_public_key_raw 107 | ].join 108 | cert_key.sign(::U2F::DIGEST.new, payload) 109 | end 110 | 111 | # The signatureData field of a SignResponse Hash. 112 | # 113 | # client_data_json - The JSON encoded clientData String. 114 | # 115 | # Returns a url-safe base64 encoded binary String. 116 | def auth_signature_data(client_data_json) 117 | ::U2F.urlsafe_encode64( 118 | [ 119 | 1, # User present 120 | self.counter += 1, 121 | auth_signature(client_data_json) 122 | ].pack('CNA*') 123 | ) 124 | end 125 | 126 | # The signature field of a signatureData field of a SignResponse Hash. 127 | # 128 | # client_data_json - The JSON encoded clientData String. 129 | # 130 | # Returns an ECDSA signature String. 131 | def auth_signature(client_data_json) 132 | data = [ 133 | ::U2F::DIGEST.digest(app_id), 134 | 1, # User present 135 | counter, 136 | ::U2F::DIGEST.digest(client_data_json) 137 | ].pack('A32CNA32') 138 | 139 | origin_key.sign(::U2F::DIGEST.new, data) 140 | end 141 | 142 | # The clientData hash as returned by registration and authentication 143 | # responses. 144 | # 145 | # typ - The String value for the 'typ' field. 146 | # challenge - The String url-safe base64 encoded challenge parameter. 147 | # 148 | # Returns a JSON encoded Hash String. 149 | def client_data(typ, challenge) 150 | JSON.dump( 151 | challenge: challenge, 152 | origin: app_id, 153 | typ: typ 154 | ) 155 | end 156 | 157 | # The appId-specific public/private key. 158 | # 159 | # Returns a OpenSSL::PKey::EC instance. 160 | def origin_key 161 | @origin_key ||= generate_ec_key 162 | end 163 | 164 | # The self-signed device attestation certificate. 165 | # 166 | # Returns a OpenSSL::X509::Certificate instance. 167 | def cert 168 | @cert ||= OpenSSL::X509::Certificate.new.tap do |c| 169 | c.subject = c.issuer = OpenSSL::X509::Name.parse(cert_subject) 170 | c.not_before = Time.now 171 | c.not_after = Time.now + 365 * 24 * 60 * 60 172 | c.public_key = cert_key 173 | c.serial = 0x1 174 | c.version = 0x0 175 | c.sign cert_key, ::U2F::DIGEST.new 176 | end 177 | end 178 | 179 | # The public key used for signing the device certificate. 180 | # 181 | # Returns a OpenSSL::PKey::EC instance. 182 | def cert_key 183 | @cert_key ||= generate_ec_key 184 | end 185 | 186 | # Generate an elliptic curve public/private key. 187 | # 188 | # Returns a OpenSSL::PKey::EC instance. 189 | def generate_ec_key 190 | OpenSSL::PKey::EC.new.tap do |ec| 191 | ec.group = OpenSSL::PKey::EC::Group.new(CURVE_NAME) 192 | ec.generate_key 193 | # https://bugs.ruby-lang.org/issues/8177 194 | ec.define_singleton_method(:private?) { private_key? } 195 | ec.define_singleton_method(:public?) { public_key? } 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby U2F 2 | 3 | [![Gem Version](https://badge.fury.io/rb/u2f.svg)](https://badge.fury.io/rb/u2f) 4 | [![security](https://hakiri.io/github/castle/ruby-u2f/master.svg)](https://hakiri.io/github/castle/ruby-u2f/master) 5 | 6 | [![Build Status](https://travis-ci.org/castle/ruby-u2f.svg?branch=API_v1_1)](https://travis-ci.org/castle/ruby-u2f) 7 | [![Code Climate](https://codeclimate.com/github/castle/ruby-u2f/badges/gpa.svg)](https://codeclimate.com/github/castle/ruby-u2f) 8 | [![Coverage Status](https://img.shields.io/coveralls/castle/ruby-u2f.svg)](https://coveralls.io/r/castle/ruby-u2f) 9 | 10 | Provides functionality for working with the server side aspects of the U2F 11 | protocol as defined in the [FIDO specifications](http://fidoalliance.org/specifications/download). To read more about U2F and how to use a U2F library, visit [developers.yubico.com/U2F](http://developers.yubico.com/U2F). 12 | 13 | ## What is U2F? 14 | 15 | U2F is an open 2-factor authentication standard that enables keychain devices, mobile phones and other devices to securely access any number of web-based services — instantly and with no drivers or client software needed. The U2F specifications were initially developed by Google, with contribution from Yubico and NXP, and are today hosted by the [FIDO Alliance](https://fidoalliance.org/). 16 | 17 | ## Working example application 18 | 19 | Check out the [example](https://github.com/castle/ruby-u2f/tree/master/example) directory for a fully working Padrino server demonstrating U2F. 20 | 21 | There is another demo application available using the [Cuba](https://github.com/soveran/cuba) framework: [cuba-u2f-demo](https://github.com/badboy/cuba-u2f-demo) and a [blog post explaining the protocol and the implementation](http://fnordig.de/2015/03/06/u2f-demo-application/). 22 | 23 | You'll need Google Chrome 41 or later to use U2F. 24 | 25 | ## Installation 26 | 27 | Add the `u2f` gem to your `Gemfile` 28 | 29 | ```ruby 30 | gem 'u2f' 31 | ``` 32 | 33 | ## Usage 34 | 35 | The U2F library has two major tasks: 36 | 37 | - **Register** new devices. 38 | - **Authenticate** previously registered devices. 39 | 40 | Each task starts by generating a challenge on the server, which is rendered to a web view, read by the browser APIs and transmitted to the plugged in U2F devices for verification. The U2F device responds and triggers a callback in the browser, and a form is posted back to your server where you verify the challenge and store the U2F device information to your database. 41 | 42 | Note that ordinarily, each user will have one or more U2F registrations (as it's a common usage pattern for users to have more than one U2F device -- for example one for regular use, and a second stored safely as a backup). While it's omitted from examples here for brevity, a new registration should typically be associated with the particular user registering. Likewise, when authenticating, queries over "all registrations" should actually be scoped to registrations associated with the particular user being authenticated. 43 | 44 | You'll need an instance of `U2F::U2F`, which is conveniently placed in an [instance method](https://github.com/castle/ruby-u2f/blob/API_v1_1/example/app/helpers/helpers.rb) on the controller. The initializer takes an **App ID** as argument. 45 | 46 | ```ruby 47 | def u2f 48 | @u2f ||= U2F::U2F.new(request.base_url) 49 | end 50 | ``` 51 | 52 | **Important:** A U2F client (e.g. Chrome) will compare the App ID with the current URI, so make sure it's the right format including schema and port, e.g. `https://demo.example.com:3000`. Check out the [App ID specification](https://developers.yubico.com/U2F/App_ID.html) for more details. 53 | 54 | ### Registration 55 | 56 | Generate the requests which will be sent to the U2F device. 57 | 58 | ```ruby 59 | # registrations_controller.rb 60 | def new 61 | # Generate one for each version of U2F, currently only `U2F_V2` 62 | @registration_requests = u2f.registration_requests 63 | 64 | # Store challenges. We need them for the verification step 65 | session[:challenges] = @registration_requests.map(&:challenge) 66 | 67 | # Fetch existing Registrations from your db and generate SignRequests 68 | key_handles = Registration.map(&:key_handle) 69 | @sign_requests = u2f.authentication_requests(key_handles) 70 | 71 | @app_id = u2f.app_id 72 | 73 | render 'registrations/new' 74 | end 75 | ``` 76 | 77 | Render a form that will be automatically posted when the U2F device reponds. 78 | 79 | ```html 80 | 81 |
82 | 83 |
84 | ``` 85 | 86 | ```javascript 87 | // render requests from server into Javascript format 88 | var appId = <%= @app_id.to_json.html_safe %> 89 | var registerRequests = <%= @registration_requests.to_json.html_safe %>; 90 | var signRequests = <%= @sign_requests.as_json.to_json.html_safe %>; 91 | 92 | u2f.register(appId, registerRequests, signRequests, function(registerResponse) { 93 | var form, reg; 94 | 95 | if (registerResponse.errorCode) { 96 | return alert("Registration error: " + registerResponse.errorCode); 97 | } 98 | 99 | form = document.forms[0]; 100 | response = document.querySelector('[name=response]'); 101 | 102 | response.value = JSON.stringify(registerResponse); 103 | 104 | form.submit(); 105 | }); 106 | ``` 107 | 108 | Catch the response on your server, verify it, and store a reference to it in your database. 109 | 110 | ```ruby 111 | # registrations_controller.rb 112 | def create 113 | response = U2F::RegisterResponse.load_from_json(params[:response]) 114 | 115 | reg = begin 116 | u2f.register!(session[:challenges], response) 117 | rescue U2F::Error => e 118 | return "Unable to register: <%= e.class.name %>" 119 | ensure 120 | session.delete(:challenges) 121 | end 122 | 123 | # save a reference to your database 124 | Registration.create!(certificate: reg.certificate, 125 | key_handle: reg.key_handle, 126 | public_key: reg.public_key, 127 | counter: reg.counter) 128 | 129 | 'Registered!' 130 | end 131 | ``` 132 | 133 | ### Authentication 134 | 135 | Generate the requests which will be sent to the U2F device. 136 | 137 | ```ruby 138 | # authentications_controller.rb 139 | def new 140 | # Fetch existing Registrations from your db 141 | key_handles = Registration.map(&:key_handle) 142 | return 'Need to register first' if key_handles.empty? 143 | 144 | # Generate SignRequests 145 | @app_id = u2f.app_id 146 | @sign_requests = u2f.authentication_requests(key_handles) 147 | @challenge = u2f.challenge 148 | 149 | # Store challenge. We need it for the verification step 150 | session[:challenge] = @challenge 151 | 152 | render 'authentications/new' 153 | end 154 | ``` 155 | 156 | Render a form that will be automatically posted when the U2F device reponds. 157 | 158 | ```html 159 | 160 |
161 | 162 |
163 | ``` 164 | 165 | ```javascript 166 | // render requests from server into Javascript format 167 | var signRequests = <%= @sign_requests.to_json.html_safe %>; 168 | var challenge = <%= @challenge.to_json.html_safe %>; 169 | var appId = <%= @app_id.to_json.html_safe %>; 170 | 171 | u2f.sign(appId, challenge, signRequests, function(signResponse) { 172 | var form, reg; 173 | 174 | if (signResponse.errorCode) { 175 | return alert("Authentication error: " + signResponse.errorCode); 176 | } 177 | 178 | form = document.forms[0]; 179 | response = document.querySelector('[name=response]'); 180 | 181 | response.value = JSON.stringify(signResponse); 182 | 183 | form.submit(); 184 | }); 185 | ``` 186 | 187 | Catch the response on your server, verify it, and bump the counter in your database reference. 188 | 189 | ```ruby 190 | # authentications_controller.rb 191 | def create 192 | response = U2F::SignResponse.load_from_json(params[:response]) 193 | 194 | registration = Registration.first(key_handle: response.key_handle) 195 | return 'Need to register first' unless registration 196 | 197 | begin 198 | u2f.authenticate!(session[:challenge], response, 199 | Base64.decode64(registration.public_key), 200 | registration.counter) 201 | rescue U2F::Error => e 202 | return "Unable to authenticate: <%= e.class.name %>" 203 | ensure 204 | session.delete(:challenge) 205 | end 206 | 207 | registration.update(counter: response.counter) 208 | 209 | 'Authenticated!' 210 | end 211 | ``` 212 | 213 | ## License 214 | 215 | MIT License. Copyright (c) 2015 by Johan Brissmyr and Sebastian Wallin 216 | -------------------------------------------------------------------------------- /example/public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v1.1.3 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /** 8 | * Correct `block` display not defined in IE 6/7/8/9 and Firefox 3. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | main, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | 26 | /** 27 | * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. 28 | */ 29 | 30 | audio, 31 | canvas, 32 | video { 33 | display: inline-block; 34 | *display: inline; 35 | *zoom: 1; 36 | } 37 | 38 | /** 39 | * Prevent modern browsers from displaying `audio` without controls. 40 | * Remove excess height in iOS 5 devices. 41 | */ 42 | 43 | audio:not([controls]) { 44 | display: none; 45 | height: 0; 46 | } 47 | 48 | /** 49 | * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4. 50 | * Known issue: no IE 6 support. 51 | */ 52 | 53 | [hidden] { 54 | display: none; 55 | } 56 | 57 | /* ========================================================================== 58 | Base 59 | ========================================================================== */ 60 | 61 | /** 62 | * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using 63 | * `em` units. 64 | * 2. Prevent iOS text size adjust after orientation change, without disabling 65 | * user zoom. 66 | */ 67 | 68 | html { 69 | font-size: 100%; /* 1 */ 70 | -ms-text-size-adjust: 100%; /* 2 */ 71 | -webkit-text-size-adjust: 100%; /* 2 */ 72 | } 73 | 74 | /** 75 | * Address `font-family` inconsistency between `textarea` and other form 76 | * elements. 77 | */ 78 | 79 | html, 80 | button, 81 | input, 82 | select, 83 | textarea { 84 | font-family: sans-serif; 85 | } 86 | 87 | /** 88 | * Address margins handled incorrectly in IE 6/7. 89 | */ 90 | 91 | body { 92 | margin: 0; 93 | } 94 | 95 | /* ========================================================================== 96 | Links 97 | ========================================================================== */ 98 | 99 | /** 100 | * Address `outline` inconsistency between Chrome and other browsers. 101 | */ 102 | 103 | a:focus { 104 | outline: thin dotted; 105 | } 106 | 107 | /** 108 | * Improve readability when focused and also mouse hovered in all browsers. 109 | */ 110 | 111 | a:active, 112 | a:hover { 113 | outline: 0; 114 | } 115 | 116 | /* ========================================================================== 117 | Typography 118 | ========================================================================== */ 119 | 120 | /** 121 | * Address font sizes and margins set differently in IE 6/7. 122 | * Address font sizes within `section` and `article` in Firefox 4+, Safari 5, 123 | * and Chrome. 124 | */ 125 | 126 | h1 { 127 | font-size: 2em; 128 | margin: 0.67em 0; 129 | } 130 | 131 | h2 { 132 | font-size: 1.5em; 133 | margin: 0.83em 0; 134 | } 135 | 136 | h3 { 137 | font-size: 1.17em; 138 | margin: 1em 0; 139 | } 140 | 141 | h4 { 142 | font-size: 1em; 143 | margin: 1.33em 0; 144 | } 145 | 146 | h5 { 147 | font-size: 0.83em; 148 | margin: 1.67em 0; 149 | } 150 | 151 | h6 { 152 | font-size: 0.67em; 153 | margin: 2.33em 0; 154 | } 155 | 156 | /** 157 | * Address styling not present in IE 7/8/9, Safari 5, and Chrome. 158 | */ 159 | 160 | abbr[title] { 161 | border-bottom: 1px dotted; 162 | } 163 | 164 | /** 165 | * Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome. 166 | */ 167 | 168 | b, 169 | strong { 170 | font-weight: bold; 171 | } 172 | 173 | blockquote { 174 | margin: 1em 40px; 175 | } 176 | 177 | /** 178 | * Address styling not present in Safari 5 and Chrome. 179 | */ 180 | 181 | dfn { 182 | font-style: italic; 183 | } 184 | 185 | /** 186 | * Address differences between Firefox and other browsers. 187 | * Known issue: no IE 6/7 normalization. 188 | */ 189 | 190 | hr { 191 | -moz-box-sizing: content-box; 192 | box-sizing: content-box; 193 | height: 0; 194 | } 195 | 196 | /** 197 | * Address styling not present in IE 6/7/8/9. 198 | */ 199 | 200 | mark { 201 | background: #ff0; 202 | color: #000; 203 | } 204 | 205 | /** 206 | * Address margins set differently in IE 6/7. 207 | */ 208 | 209 | p, 210 | pre { 211 | margin: 1em 0; 212 | } 213 | 214 | /** 215 | * Correct font family set oddly in IE 6, Safari 4/5, and Chrome. 216 | */ 217 | 218 | code, 219 | kbd, 220 | pre, 221 | samp { 222 | font-family: monospace, serif; 223 | _font-family: 'courier new', monospace; 224 | font-size: 1em; 225 | } 226 | 227 | /** 228 | * Improve readability of pre-formatted text in all browsers. 229 | */ 230 | 231 | pre { 232 | white-space: pre; 233 | white-space: pre-wrap; 234 | word-wrap: break-word; 235 | } 236 | 237 | /** 238 | * Address CSS quotes not supported in IE 6/7. 239 | */ 240 | 241 | q { 242 | quotes: none; 243 | } 244 | 245 | /** 246 | * Address `quotes` property not supported in Safari 4. 247 | */ 248 | 249 | q:before, 250 | q:after { 251 | content: ''; 252 | content: none; 253 | } 254 | 255 | /** 256 | * Address inconsistent and variable font size in all browsers. 257 | */ 258 | 259 | small { 260 | font-size: 80%; 261 | } 262 | 263 | /** 264 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 265 | */ 266 | 267 | sub, 268 | sup { 269 | font-size: 75%; 270 | line-height: 0; 271 | position: relative; 272 | vertical-align: baseline; 273 | } 274 | 275 | sup { 276 | top: -0.5em; 277 | } 278 | 279 | sub { 280 | bottom: -0.25em; 281 | } 282 | 283 | /* ========================================================================== 284 | Lists 285 | ========================================================================== */ 286 | 287 | /** 288 | * Address margins set differently in IE 6/7. 289 | */ 290 | 291 | dl, 292 | menu, 293 | ol, 294 | ul { 295 | margin: 1em 0; 296 | } 297 | 298 | dd { 299 | margin: 0 0 0 40px; 300 | } 301 | 302 | /** 303 | * Address paddings set differently in IE 6/7. 304 | */ 305 | 306 | menu, 307 | ol, 308 | ul { 309 | padding: 0 0 0 40px; 310 | } 311 | 312 | /** 313 | * Correct list images handled incorrectly in IE 7. 314 | */ 315 | 316 | nav ul, 317 | nav ol { 318 | list-style: none; 319 | list-style-image: none; 320 | } 321 | 322 | /* ========================================================================== 323 | Embedded content 324 | ========================================================================== */ 325 | 326 | /** 327 | * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3. 328 | * 2. Improve image quality when scaled in IE 7. 329 | */ 330 | 331 | img { 332 | border: 0; /* 1 */ 333 | -ms-interpolation-mode: bicubic; /* 2 */ 334 | } 335 | 336 | /** 337 | * Correct overflow displayed oddly in IE 9. 338 | */ 339 | 340 | svg:not(:root) { 341 | overflow: hidden; 342 | } 343 | 344 | /* ========================================================================== 345 | Figures 346 | ========================================================================== */ 347 | 348 | /** 349 | * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11. 350 | */ 351 | 352 | figure { 353 | margin: 0; 354 | } 355 | 356 | /* ========================================================================== 357 | Forms 358 | ========================================================================== */ 359 | 360 | /** 361 | * Correct margin displayed oddly in IE 6/7. 362 | */ 363 | 364 | form { 365 | margin: 0; 366 | } 367 | 368 | /** 369 | * Define consistent border, margin, and padding. 370 | */ 371 | 372 | fieldset { 373 | border: 1px solid #c0c0c0; 374 | margin: 0 2px; 375 | padding: 0.35em 0.625em 0.75em; 376 | } 377 | 378 | /** 379 | * 1. Correct color not being inherited in IE 6/7/8/9. 380 | * 2. Correct text not wrapping in Firefox 3. 381 | * 3. Correct alignment displayed oddly in IE 6/7. 382 | */ 383 | 384 | legend { 385 | border: 0; /* 1 */ 386 | padding: 0; 387 | white-space: normal; /* 2 */ 388 | *margin-left: -7px; /* 3 */ 389 | } 390 | 391 | /** 392 | * 1. Correct font size not being inherited in all browsers. 393 | * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5, 394 | * and Chrome. 395 | * 3. Improve appearance and consistency in all browsers. 396 | */ 397 | 398 | button, 399 | input, 400 | select, 401 | textarea { 402 | font-size: 100%; /* 1 */ 403 | margin: 0; /* 2 */ 404 | vertical-align: baseline; /* 3 */ 405 | *vertical-align: middle; /* 3 */ 406 | } 407 | 408 | /** 409 | * Address Firefox 3+ setting `line-height` on `input` using `!important` in 410 | * the UA stylesheet. 411 | */ 412 | 413 | button, 414 | input { 415 | line-height: normal; 416 | } 417 | 418 | /** 419 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 420 | * All other form control elements do not inherit `text-transform` values. 421 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+. 422 | * Correct `select` style inheritance in Firefox 4+ and Opera. 423 | */ 424 | 425 | button, 426 | select { 427 | text-transform: none; 428 | } 429 | 430 | /** 431 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 432 | * and `video` controls. 433 | * 2. Correct inability to style clickable `input` types in iOS. 434 | * 3. Improve usability and consistency of cursor style between image-type 435 | * `input` and others. 436 | * 4. Remove inner spacing in IE 7 without affecting normal text inputs. 437 | * Known issue: inner spacing remains in IE 6. 438 | */ 439 | 440 | button, 441 | html input[type="button"], /* 1 */ 442 | input[type="reset"], 443 | input[type="submit"] { 444 | -webkit-appearance: button; /* 2 */ 445 | cursor: pointer; /* 3 */ 446 | *overflow: visible; /* 4 */ 447 | } 448 | 449 | /** 450 | * Re-set default cursor for disabled elements. 451 | */ 452 | 453 | button[disabled], 454 | html input[disabled] { 455 | cursor: default; 456 | } 457 | 458 | /** 459 | * 1. Address box sizing set to content-box in IE 8/9. 460 | * 2. Remove excess padding in IE 8/9. 461 | * 3. Remove excess padding in IE 7. 462 | * Known issue: excess padding remains in IE 6. 463 | */ 464 | 465 | input[type="checkbox"], 466 | input[type="radio"] { 467 | box-sizing: border-box; /* 1 */ 468 | padding: 0; /* 2 */ 469 | *height: 13px; /* 3 */ 470 | *width: 13px; /* 3 */ 471 | } 472 | 473 | /** 474 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 475 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 476 | * (include `-moz` to future-proof). 477 | */ 478 | 479 | input[type="search"] { 480 | -webkit-appearance: textfield; /* 1 */ 481 | -moz-box-sizing: content-box; 482 | -webkit-box-sizing: content-box; /* 2 */ 483 | box-sizing: content-box; 484 | } 485 | 486 | /** 487 | * Remove inner padding and search cancel button in Safari 5 and Chrome 488 | * on OS X. 489 | */ 490 | 491 | input[type="search"]::-webkit-search-cancel-button, 492 | input[type="search"]::-webkit-search-decoration { 493 | -webkit-appearance: none; 494 | } 495 | 496 | /** 497 | * Remove inner padding and border in Firefox 3+. 498 | */ 499 | 500 | button::-moz-focus-inner, 501 | input::-moz-focus-inner { 502 | border: 0; 503 | padding: 0; 504 | } 505 | 506 | /** 507 | * 1. Remove default vertical scrollbar in IE 6/7/8/9. 508 | * 2. Improve readability and alignment in all browsers. 509 | */ 510 | 511 | textarea { 512 | overflow: auto; /* 1 */ 513 | vertical-align: top; /* 2 */ 514 | } 515 | 516 | /* ========================================================================== 517 | Tables 518 | ========================================================================== */ 519 | 520 | /** 521 | * Remove most spacing between table cells. 522 | */ 523 | 524 | table { 525 | border-collapse: collapse; 526 | border-spacing: 0; 527 | } 528 | -------------------------------------------------------------------------------- /example/public/javascripts/u2f-api.js: -------------------------------------------------------------------------------- 1 | //Copyright 2014-2015 Google Inc. All rights reserved. 2 | 3 | //Use of this source code is governed by a BSD-style 4 | //license that can be found in the LICENSE file or at 5 | //https://developers.google.com/open-source/licenses/bsd 6 | 7 | /** 8 | * @fileoverview The U2F api. 9 | */ 10 | 'use strict'; 11 | 12 | 13 | /** 14 | * Namespace for the U2F api. 15 | * @type {Object} 16 | */ 17 | var u2f = u2f || {}; 18 | 19 | /** 20 | * FIDO U2F Javascript API Version 21 | * @number 22 | */ 23 | var js_api_version; 24 | 25 | /** 26 | * The U2F extension id 27 | * @const {string} 28 | */ 29 | // The Chrome packaged app extension ID. 30 | // Uncomment this if you want to deploy a server instance that uses 31 | // the package Chrome app and does not require installing the U2F Chrome extension. 32 | u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; 33 | // The U2F Chrome extension ID. 34 | // Uncomment this if you want to deploy a server instance that uses 35 | // the U2F Chrome extension to authenticate. 36 | // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; 37 | 38 | 39 | /** 40 | * Message types for messsages to/from the extension 41 | * @const 42 | * @enum {string} 43 | */ 44 | u2f.MessageTypes = { 45 | 'U2F_REGISTER_REQUEST': 'u2f_register_request', 46 | 'U2F_REGISTER_RESPONSE': 'u2f_register_response', 47 | 'U2F_SIGN_REQUEST': 'u2f_sign_request', 48 | 'U2F_SIGN_RESPONSE': 'u2f_sign_response', 49 | 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', 50 | 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' 51 | }; 52 | 53 | 54 | /** 55 | * Response status codes 56 | * @const 57 | * @enum {number} 58 | */ 59 | u2f.ErrorCodes = { 60 | 'OK': 0, 61 | 'OTHER_ERROR': 1, 62 | 'BAD_REQUEST': 2, 63 | 'CONFIGURATION_UNSUPPORTED': 3, 64 | 'DEVICE_INELIGIBLE': 4, 65 | 'TIMEOUT': 5 66 | }; 67 | 68 | 69 | /** 70 | * A message for registration requests 71 | * @typedef {{ 72 | * type: u2f.MessageTypes, 73 | * appId: ?string, 74 | * timeoutSeconds: ?number, 75 | * requestId: ?number 76 | * }} 77 | */ 78 | u2f.U2fRequest; 79 | 80 | 81 | /** 82 | * A message for registration responses 83 | * @typedef {{ 84 | * type: u2f.MessageTypes, 85 | * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), 86 | * requestId: ?number 87 | * }} 88 | */ 89 | u2f.U2fResponse; 90 | 91 | 92 | /** 93 | * An error object for responses 94 | * @typedef {{ 95 | * errorCode: u2f.ErrorCodes, 96 | * errorMessage: ?string 97 | * }} 98 | */ 99 | u2f.Error; 100 | 101 | /** 102 | * Data object for a single sign request. 103 | * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} 104 | */ 105 | u2f.Transport; 106 | 107 | 108 | /** 109 | * Data object for a single sign request. 110 | * @typedef {Array} 111 | */ 112 | u2f.Transports; 113 | 114 | /** 115 | * Data object for a single sign request. 116 | * @typedef {{ 117 | * version: string, 118 | * challenge: string, 119 | * keyHandle: string, 120 | * appId: string 121 | * }} 122 | */ 123 | u2f.SignRequest; 124 | 125 | 126 | /** 127 | * Data object for a sign response. 128 | * @typedef {{ 129 | * keyHandle: string, 130 | * signatureData: string, 131 | * clientData: string 132 | * }} 133 | */ 134 | u2f.SignResponse; 135 | 136 | 137 | /** 138 | * Data object for a registration request. 139 | * @typedef {{ 140 | * version: string, 141 | * challenge: string 142 | * }} 143 | */ 144 | u2f.RegisterRequest; 145 | 146 | 147 | /** 148 | * Data object for a registration response. 149 | * @typedef {{ 150 | * version: string, 151 | * keyHandle: string, 152 | * transports: Transports, 153 | * appId: string 154 | * }} 155 | */ 156 | u2f.RegisterResponse; 157 | 158 | 159 | /** 160 | * Data object for a registered key. 161 | * @typedef {{ 162 | * version: string, 163 | * keyHandle: string, 164 | * transports: ?Transports, 165 | * appId: ?string 166 | * }} 167 | */ 168 | u2f.RegisteredKey; 169 | 170 | 171 | /** 172 | * Data object for a get API register response. 173 | * @typedef {{ 174 | * js_api_version: number 175 | * }} 176 | */ 177 | u2f.GetJsApiVersionResponse; 178 | 179 | 180 | //Low level MessagePort API support 181 | 182 | /** 183 | * Sets up a MessagePort to the U2F extension using the 184 | * available mechanisms. 185 | * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback 186 | */ 187 | u2f.getMessagePort = function(callback) { 188 | if (typeof chrome != 'undefined' && chrome.runtime) { 189 | // The actual message here does not matter, but we need to get a reply 190 | // for the callback to run. Thus, send an empty signature request 191 | // in order to get a failure response. 192 | var msg = { 193 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 194 | signRequests: [] 195 | }; 196 | chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { 197 | if (!chrome.runtime.lastError) { 198 | // We are on a whitelisted origin and can talk directly 199 | // with the extension. 200 | u2f.getChromeRuntimePort_(callback); 201 | } else { 202 | // chrome.runtime was available, but we couldn't message 203 | // the extension directly, use iframe 204 | u2f.getIframePort_(callback); 205 | } 206 | }); 207 | } else if (u2f.isAndroidChrome_()) { 208 | u2f.getAuthenticatorPort_(callback); 209 | } else if (u2f.isIosChrome_()) { 210 | u2f.getIosPort_(callback); 211 | } else { 212 | // chrome.runtime was not available at all, which is normal 213 | // when this origin doesn't have access to any extensions. 214 | u2f.getIframePort_(callback); 215 | } 216 | }; 217 | 218 | /** 219 | * Detect chrome running on android based on the browser's useragent. 220 | * @private 221 | */ 222 | u2f.isAndroidChrome_ = function() { 223 | var userAgent = navigator.userAgent; 224 | return userAgent.indexOf('Chrome') != -1 && 225 | userAgent.indexOf('Android') != -1; 226 | }; 227 | 228 | /** 229 | * Detect chrome running on iOS based on the browser's platform. 230 | * @private 231 | */ 232 | u2f.isIosChrome_ = function() { 233 | return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; 234 | }; 235 | 236 | /** 237 | * Connects directly to the extension via chrome.runtime.connect. 238 | * @param {function(u2f.WrappedChromeRuntimePort_)} callback 239 | * @private 240 | */ 241 | u2f.getChromeRuntimePort_ = function(callback) { 242 | var port = chrome.runtime.connect(u2f.EXTENSION_ID, 243 | {'includeTlsChannelId': true}); 244 | setTimeout(function() { 245 | callback(new u2f.WrappedChromeRuntimePort_(port)); 246 | }, 0); 247 | }; 248 | 249 | /** 250 | * Return a 'port' abstraction to the Authenticator app. 251 | * @param {function(u2f.WrappedAuthenticatorPort_)} callback 252 | * @private 253 | */ 254 | u2f.getAuthenticatorPort_ = function(callback) { 255 | setTimeout(function() { 256 | callback(new u2f.WrappedAuthenticatorPort_()); 257 | }, 0); 258 | }; 259 | 260 | /** 261 | * Return a 'port' abstraction to the iOS client app. 262 | * @param {function(u2f.WrappedIosPort_)} callback 263 | * @private 264 | */ 265 | u2f.getIosPort_ = function(callback) { 266 | setTimeout(function() { 267 | callback(new u2f.WrappedIosPort_()); 268 | }, 0); 269 | }; 270 | 271 | /** 272 | * A wrapper for chrome.runtime.Port that is compatible with MessagePort. 273 | * @param {Port} port 274 | * @constructor 275 | * @private 276 | */ 277 | u2f.WrappedChromeRuntimePort_ = function(port) { 278 | this.port_ = port; 279 | }; 280 | 281 | /** 282 | * Format and return a sign request compliant with the JS API version supported by the extension. 283 | * @param {Array} signRequests 284 | * @param {number} timeoutSeconds 285 | * @param {number} reqId 286 | * @return {Object} 287 | */ 288 | u2f.formatSignRequest_ = 289 | function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { 290 | if (js_api_version === undefined || js_api_version < 1.1) { 291 | // Adapt request to the 1.0 JS API 292 | var signRequests = []; 293 | for (var i = 0; i < registeredKeys.length; i++) { 294 | signRequests[i] = { 295 | version: registeredKeys[i].version, 296 | challenge: challenge, 297 | keyHandle: registeredKeys[i].keyHandle, 298 | appId: appId 299 | }; 300 | } 301 | return { 302 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 303 | signRequests: signRequests, 304 | timeoutSeconds: timeoutSeconds, 305 | requestId: reqId 306 | }; 307 | } 308 | // JS 1.1 API 309 | return { 310 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 311 | appId: appId, 312 | challenge: challenge, 313 | registeredKeys: registeredKeys, 314 | timeoutSeconds: timeoutSeconds, 315 | requestId: reqId 316 | }; 317 | }; 318 | 319 | /** 320 | * Format and return a register request compliant with the JS API version supported by the extension.. 321 | * @param {Array} signRequests 322 | * @param {Array} signRequests 323 | * @param {number} timeoutSeconds 324 | * @param {number} reqId 325 | * @return {Object} 326 | */ 327 | u2f.formatRegisterRequest_ = 328 | function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { 329 | if (js_api_version === undefined || js_api_version < 1.1) { 330 | // Adapt request to the 1.0 JS API 331 | for (var i = 0; i < registerRequests.length; i++) { 332 | registerRequests[i].appId = appId; 333 | } 334 | var signRequests = []; 335 | for (var i = 0; i < registeredKeys.length; i++) { 336 | signRequests[i] = { 337 | version: registeredKeys[i].version, 338 | challenge: registerRequests[0], 339 | keyHandle: registeredKeys[i].keyHandle, 340 | appId: appId 341 | }; 342 | } 343 | return { 344 | type: u2f.MessageTypes.U2F_REGISTER_REQUEST, 345 | signRequests: signRequests, 346 | registerRequests: registerRequests, 347 | timeoutSeconds: timeoutSeconds, 348 | requestId: reqId 349 | }; 350 | } 351 | // JS 1.1 API 352 | return { 353 | type: u2f.MessageTypes.U2F_REGISTER_REQUEST, 354 | appId: appId, 355 | registerRequests: registerRequests, 356 | registeredKeys: registeredKeys, 357 | timeoutSeconds: timeoutSeconds, 358 | requestId: reqId 359 | }; 360 | }; 361 | 362 | 363 | /** 364 | * Posts a message on the underlying channel. 365 | * @param {Object} message 366 | */ 367 | u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { 368 | this.port_.postMessage(message); 369 | }; 370 | 371 | 372 | /** 373 | * Emulates the HTML 5 addEventListener interface. Works only for the 374 | * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. 375 | * @param {string} eventName 376 | * @param {function({data: Object})} handler 377 | */ 378 | u2f.WrappedChromeRuntimePort_.prototype.addEventListener = 379 | function(eventName, handler) { 380 | var name = eventName.toLowerCase(); 381 | if (name == 'message' || name == 'onmessage') { 382 | this.port_.onMessage.addListener(function(message) { 383 | // Emulate a minimal MessageEvent object 384 | handler({'data': message}); 385 | }); 386 | } else { 387 | console.error('WrappedChromeRuntimePort only supports onMessage'); 388 | } 389 | }; 390 | 391 | /** 392 | * Wrap the Authenticator app with a MessagePort interface. 393 | * @constructor 394 | * @private 395 | */ 396 | u2f.WrappedAuthenticatorPort_ = function() { 397 | this.requestId_ = -1; 398 | this.requestObject_ = null; 399 | } 400 | 401 | /** 402 | * Launch the Authenticator intent. 403 | * @param {Object} message 404 | */ 405 | u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { 406 | var intentUrl = 407 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + 408 | ';S.request=' + encodeURIComponent(JSON.stringify(message)) + 409 | ';end'; 410 | document.location = intentUrl; 411 | }; 412 | 413 | /** 414 | * Tells what type of port this is. 415 | * @return {String} port type 416 | */ 417 | u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { 418 | return "WrappedAuthenticatorPort_"; 419 | }; 420 | 421 | 422 | /** 423 | * Emulates the HTML 5 addEventListener interface. 424 | * @param {string} eventName 425 | * @param {function({data: Object})} handler 426 | */ 427 | u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { 428 | var name = eventName.toLowerCase(); 429 | if (name == 'message') { 430 | var self = this; 431 | /* Register a callback to that executes when 432 | * chrome injects the response. */ 433 | window.addEventListener( 434 | 'message', self.onRequestUpdate_.bind(self, handler), false); 435 | } else { 436 | console.error('WrappedAuthenticatorPort only supports message'); 437 | } 438 | }; 439 | 440 | /** 441 | * Callback invoked when a response is received from the Authenticator. 442 | * @param function({data: Object}) callback 443 | * @param {Object} message message Object 444 | */ 445 | u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = 446 | function(callback, message) { 447 | var messageObject = JSON.parse(message.data); 448 | var intentUrl = messageObject['intentURL']; 449 | 450 | var errorCode = messageObject['errorCode']; 451 | var responseObject = null; 452 | if (messageObject.hasOwnProperty('data')) { 453 | responseObject = /** @type {Object} */ ( 454 | JSON.parse(messageObject['data'])); 455 | } 456 | 457 | callback({'data': responseObject}); 458 | }; 459 | 460 | /** 461 | * Base URL for intents to Authenticator. 462 | * @const 463 | * @private 464 | */ 465 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = 466 | 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; 467 | 468 | /** 469 | * Wrap the iOS client app with a MessagePort interface. 470 | * @constructor 471 | * @private 472 | */ 473 | u2f.WrappedIosPort_ = function() {}; 474 | 475 | /** 476 | * Launch the iOS client app request 477 | * @param {Object} message 478 | */ 479 | u2f.WrappedIosPort_.prototype.postMessage = function(message) { 480 | var str = JSON.stringify(message); 481 | var url = "u2f://auth?" + encodeURI(str); 482 | location.replace(url); 483 | }; 484 | 485 | /** 486 | * Tells what type of port this is. 487 | * @return {String} port type 488 | */ 489 | u2f.WrappedIosPort_.prototype.getPortType = function() { 490 | return "WrappedIosPort_"; 491 | }; 492 | 493 | /** 494 | * Emulates the HTML 5 addEventListener interface. 495 | * @param {string} eventName 496 | * @param {function({data: Object})} handler 497 | */ 498 | u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { 499 | var name = eventName.toLowerCase(); 500 | if (name !== 'message') { 501 | console.error('WrappedIosPort only supports message'); 502 | } 503 | }; 504 | 505 | /** 506 | * Sets up an embedded trampoline iframe, sourced from the extension. 507 | * @param {function(MessagePort)} callback 508 | * @private 509 | */ 510 | u2f.getIframePort_ = function(callback) { 511 | // Create the iframe 512 | var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; 513 | var iframe = document.createElement('iframe'); 514 | iframe.src = iframeOrigin + '/u2f-comms.html'; 515 | iframe.setAttribute('style', 'display:none'); 516 | document.body.appendChild(iframe); 517 | 518 | var channel = new MessageChannel(); 519 | var ready = function(message) { 520 | if (message.data == 'ready') { 521 | channel.port1.removeEventListener('message', ready); 522 | callback(channel.port1); 523 | } else { 524 | console.error('First event on iframe port was not "ready"'); 525 | } 526 | }; 527 | channel.port1.addEventListener('message', ready); 528 | channel.port1.start(); 529 | 530 | iframe.addEventListener('load', function() { 531 | // Deliver the port to the iframe and initialize 532 | iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); 533 | }); 534 | }; 535 | 536 | 537 | //High-level JS API 538 | 539 | /** 540 | * Default extension response timeout in seconds. 541 | * @const 542 | */ 543 | u2f.EXTENSION_TIMEOUT_SEC = 30; 544 | 545 | /** 546 | * A singleton instance for a MessagePort to the extension. 547 | * @type {MessagePort|u2f.WrappedChromeRuntimePort_} 548 | * @private 549 | */ 550 | u2f.port_ = null; 551 | 552 | /** 553 | * Callbacks waiting for a port 554 | * @type {Array} 555 | * @private 556 | */ 557 | u2f.waitingForPort_ = []; 558 | 559 | /** 560 | * A counter for requestIds. 561 | * @type {number} 562 | * @private 563 | */ 564 | u2f.reqCounter_ = 0; 565 | 566 | /** 567 | * A map from requestIds to client callbacks 568 | * @type {Object.} 570 | * @private 571 | */ 572 | u2f.callbackMap_ = {}; 573 | 574 | /** 575 | * Creates or retrieves the MessagePort singleton to use. 576 | * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback 577 | * @private 578 | */ 579 | u2f.getPortSingleton_ = function(callback) { 580 | if (u2f.port_) { 581 | callback(u2f.port_); 582 | } else { 583 | if (u2f.waitingForPort_.length == 0) { 584 | u2f.getMessagePort(function(port) { 585 | u2f.port_ = port; 586 | u2f.port_.addEventListener('message', 587 | /** @type {function(Event)} */ (u2f.responseHandler_)); 588 | 589 | // Careful, here be async callbacks. Maybe. 590 | while (u2f.waitingForPort_.length) 591 | u2f.waitingForPort_.shift()(u2f.port_); 592 | }); 593 | } 594 | u2f.waitingForPort_.push(callback); 595 | } 596 | }; 597 | 598 | /** 599 | * Handles response messages from the extension. 600 | * @param {MessageEvent.} message 601 | * @private 602 | */ 603 | u2f.responseHandler_ = function(message) { 604 | var response = message.data; 605 | var reqId = response['requestId']; 606 | if (!reqId || !u2f.callbackMap_[reqId]) { 607 | console.error('Unknown or missing requestId in response.'); 608 | return; 609 | } 610 | var cb = u2f.callbackMap_[reqId]; 611 | delete u2f.callbackMap_[reqId]; 612 | cb(response['responseData']); 613 | }; 614 | 615 | /** 616 | * Dispatches an array of sign requests to available U2F tokens. 617 | * If the JS API version supported by the extension is unknown, it first sends a 618 | * message to the extension to find out the supported API version and then it sends 619 | * the sign request. 620 | * @param {string=} appId 621 | * @param {string=} challenge 622 | * @param {Array} registeredKeys 623 | * @param {function((u2f.Error|u2f.SignResponse))} callback 624 | * @param {number=} opt_timeoutSeconds 625 | */ 626 | u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { 627 | if (js_api_version === undefined) { 628 | // Send a message to get the extension to JS API version, then send the actual sign request. 629 | u2f.getApiVersion( 630 | function (response) { 631 | js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; 632 | console.log("Extension JS API Version: ", js_api_version); 633 | u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); 634 | }); 635 | } else { 636 | // We know the JS API version. Send the actual sign request in the supported API version. 637 | u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); 638 | } 639 | }; 640 | 641 | /** 642 | * Dispatches an array of sign requests to available U2F tokens. 643 | * @param {string=} appId 644 | * @param {string=} challenge 645 | * @param {Array} registeredKeys 646 | * @param {function((u2f.Error|u2f.SignResponse))} callback 647 | * @param {number=} opt_timeoutSeconds 648 | */ 649 | u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { 650 | u2f.getPortSingleton_(function(port) { 651 | var reqId = ++u2f.reqCounter_; 652 | u2f.callbackMap_[reqId] = callback; 653 | var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? 654 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); 655 | var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); 656 | port.postMessage(req); 657 | }); 658 | }; 659 | 660 | /** 661 | * Dispatches register requests to available U2F tokens. An array of sign 662 | * requests identifies already registered tokens. 663 | * If the JS API version supported by the extension is unknown, it first sends a 664 | * message to the extension to find out the supported API version and then it sends 665 | * the register request. 666 | * @param {string=} appId 667 | * @param {Array} registerRequests 668 | * @param {Array} registeredKeys 669 | * @param {function((u2f.Error|u2f.RegisterResponse))} callback 670 | * @param {number=} opt_timeoutSeconds 671 | */ 672 | u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { 673 | if (js_api_version === undefined) { 674 | // Send a message to get the extension to JS API version, then send the actual register request. 675 | u2f.getApiVersion( 676 | function (response) { 677 | js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; 678 | console.log("Extension JS API Version: ", js_api_version); 679 | u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, 680 | callback, opt_timeoutSeconds); 681 | }); 682 | } else { 683 | // We know the JS API version. Send the actual register request in the supported API version. 684 | u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, 685 | callback, opt_timeoutSeconds); 686 | } 687 | }; 688 | 689 | /** 690 | * Dispatches register requests to available U2F tokens. An array of sign 691 | * requests identifies already registered tokens. 692 | * @param {string=} appId 693 | * @param {Array} registerRequests 694 | * @param {Array} registeredKeys 695 | * @param {function((u2f.Error|u2f.RegisterResponse))} callback 696 | * @param {number=} opt_timeoutSeconds 697 | */ 698 | u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { 699 | u2f.getPortSingleton_(function(port) { 700 | var reqId = ++u2f.reqCounter_; 701 | u2f.callbackMap_[reqId] = callback; 702 | var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? 703 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); 704 | var req = u2f.formatRegisterRequest_( 705 | appId, registeredKeys, registerRequests, timeoutSeconds, reqId); 706 | port.postMessage(req); 707 | }); 708 | }; 709 | 710 | 711 | /** 712 | * Dispatches a message to the extension to find out the supported 713 | * JS API version. 714 | * If the user is on a mobile phone and is thus using Google Authenticator instead 715 | * of the Chrome extension, don't send the request and simply return 0. 716 | * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback 717 | * @param {number=} opt_timeoutSeconds 718 | */ 719 | u2f.getApiVersion = function(callback, opt_timeoutSeconds) { 720 | u2f.getPortSingleton_(function(port) { 721 | // If we are using Android Google Authenticator or iOS client app, 722 | // do not fire an intent to ask which JS API version to use. 723 | if (port.getPortType) { 724 | var apiVersion; 725 | switch (port.getPortType()) { 726 | case 'WrappedIosPort_': 727 | case 'WrappedAuthenticatorPort_': 728 | apiVersion = 1.1; 729 | break; 730 | 731 | default: 732 | apiVersion = 0; 733 | break; 734 | } 735 | callback({ 'js_api_version': apiVersion }); 736 | return; 737 | } 738 | var reqId = ++u2f.reqCounter_; 739 | u2f.callbackMap_[reqId] = callback; 740 | var req = { 741 | type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, 742 | timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? 743 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), 744 | requestId: reqId 745 | }; 746 | port.postMessage(req); 747 | }); 748 | }; 749 | --------------------------------------------------------------------------------