u2f-0.2.1/0000755000175600017570000000000012730505562011254 5ustar pravipraviu2f-0.2.1/spec/0000755000175600017570000000000012730505562012206 5ustar pravipraviu2f-0.2.1/spec/spec_helper.rb0000644000175600017570000000041312730505562015022 0ustar pravipravirequire 'simplecov' require 'coveralls' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter ] SimpleCov.start do add_filter 'spec' end require 'json_expressions/rspec' require 'u2f' u2f-0.2.1/spec/lib/0000755000175600017570000000000012730505562012754 5ustar pravipraviu2f-0.2.1/spec/lib/client_data_spec.rb0000644000175600017570000000166012730505562016565 0ustar pravipravirequire 'spec_helper.rb' describe U2F::ClientData do let(:type) { '' } let(:registration_type) { U2F::ClientData::REGISTRATION_TYP } let(:authentication_type) { U2F::ClientData::AUTHENTICATION_TYP } let(:client_data) do cd = U2F::ClientData.new cd.typ = type cd end describe '#registration?' do subject { client_data.registration? } context 'for correct type' do let(:type) { registration_type } it { is_expected.to be_truthy } end context 'for incorrect type' do let(:type) { authentication_type } it { is_expected.to be_falsey } end end describe '#authentication?' do subject { client_data.authentication? } context 'for correct type' do let(:type) { authentication_type } it { is_expected.to be_truthy } end context 'for incorrect type' do let(:type) { registration_type } it { is_expected.to be_falsey } end end end u2f-0.2.1/spec/lib/register_request_spec.rb0000644000175600017570000000071312730505562017710 0ustar pravipravirequire 'spec_helper' describe U2F::RegisterRequest do let(:app_id) { 'http://example.com' } let(:challenge) { 'fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g' } let(:sign_request) do U2F::RegisterRequest.new(challenge, app_id) end describe '#to_json' do subject { sign_request.to_json } it do is_expected.to match_json_expression( version: String, appId: String, challenge: String ) end end endu2f-0.2.1/spec/lib/u2f_spec.rb0000644000175600017570000001026512730505562015013 0ustar pravipravi require 'spec_helper' describe U2F do let(:app_id) { 'http://demo.example.com' } let(:device_challenge) { U2F.urlsafe_encode64(SecureRandom.random_bytes(32)) } let(:auth_challenge) { device_challenge } let(:u2f) { U2F::U2F.new(app_id) } let(:device) { U2F::FakeU2F.new(app_id) } let(:key_handle) { U2F.urlsafe_encode64(device.key_handle_raw) } let(:certificate) { Base64.strict_encode64(device.cert_raw) } let(:public_key) { device.origin_public_key_raw } let(:register_response_json) { device.register_response(device_challenge) } let(:sign_response_json) { device.sign_response(device_challenge) } let(:registration) do U2F::Registration.new(key_handle, public_key, certificate) end let(:register_response) do U2F::RegisterResponse.load_from_json(register_response_json) end let(:sign_response) do U2F::SignResponse.load_from_json sign_response_json end let(:sign_request) do U2F::SignRequest.new(key_handle, auth_challenge, app_id) end describe '#authentication_requests' do let(:requests) { u2f.authentication_requests(key_handle) } it 'returns an array of requests' do expect(requests).to be_an Array requests.each { |r| expect(r).to be_a U2F::SignRequest } end end describe '#authenticate!' do let(:counter) { registration.counter } let(:reg_public_key) { registration.public_key } let (:u2f_authenticate) do u2f.authenticate!(auth_challenge, sign_response, reg_public_key, counter) end context 'with correct parameters' do it 'does not raise an error' do expect { u2f_authenticate }.to_not raise_error end end context 'with incorrect challenge' do let(:auth_challenge) { 'incorrect' } it 'raises NoMatchingRequestError' do expect { u2f_authenticate }.to raise_error(U2F::NoMatchingRequestError) end end context 'with incorrect counter' do let(:counter) { 1000 } it 'raises CounterTooLowError' do expect { u2f_authenticate }.to raise_error(U2F::CounterTooLowError) end end context 'with incorrect counter' do let(:reg_public_key) { "\x00" } it 'raises CounterToLowError' do expect { u2f_authenticate }.to raise_error(U2F::PublicKeyDecodeError) end end end describe '#registration_requests' do let(:requests) { u2f.registration_requests } it 'returns an array of requests' do expect(requests).to be_an Array requests.each { |r| expect(r).to be_a U2F::RegisterRequest } end end describe '#register!' do context 'with correct registration data' do it 'returns a registration' do reg = nil expect { reg = u2f.register!(auth_challenge, register_response) }.to_not raise_error expect(reg.key_handle).to eq key_handle end it 'accepts an array of challenges' do reg = u2f.register!(['another-challenge', auth_challenge], register_response) expect(reg).to be_a U2F::Registration end end context 'with unknown challenge' do let(:auth_challenge) { 'non-matching' } it 'raises an UnmatchedChallengeError' do expect { u2f.register!(auth_challenge, register_response) }.to raise_error(U2F::UnmatchedChallengeError) end end end describe '::public_key_pem' do context 'with correct key' do it 'wraps the result' do pem = U2F::U2F.public_key_pem public_key expect(pem).to start_with '-----BEGIN PUBLIC KEY-----' expect(pem).to end_with '-----END PUBLIC KEY-----' end end context 'with invalid key' do let(:public_key) { U2F.urlsafe_decode64('YV6FVSmH0ObY1cBRCsYJZ/CXF1gKsL+DW46rMfpeymtDZted2Ut2BraszUK1wg1+YJ4Bxt6r24WHNUYqKgeaSq8=') } it 'fails when first byte of the key is not 0x04' do expect { U2F::U2F.public_key_pem public_key }.to raise_error(U2F::PublicKeyDecodeError) end end context 'with truncated key' do let(:public_key) { U2F.urlsafe_decode64('BJhSPkR3Rmgl') } it 'fails when key is to short' do expect { U2F::U2F.public_key_pem public_key }.to raise_error(U2F::PublicKeyDecodeError) end end end end u2f-0.2.1/spec/lib/sign_response_spec.rb0000644000175600017570000000230612730505562017172 0ustar pravipravirequire 'spec_helper.rb' describe U2F::SignResponse do let(:app_id) { 'http://demo.example.com' } let(:challenge) { U2F.urlsafe_encode64(SecureRandom.random_bytes(32)) } let(:device) { U2F::FakeU2F.new(app_id) } let(:json_response) { device.sign_response(challenge) } let(:sign_response) { U2F::SignResponse.load_from_json json_response } let(:public_key_pem) { U2F::U2F.public_key_pem(device.origin_public_key_raw) } describe '#counter' do subject { sign_response.counter } it { is_expected.to be device.counter } end describe '#user_present?' do subject { sign_response.user_present? } it { is_expected.to be true } end describe '#verify with correct app id' do subject { sign_response.verify(app_id, public_key_pem) } it { is_expected.to be_truthy} end describe '#verify with wrong app id' do subject { sign_response.verify("other app", public_key_pem) } it { is_expected.to be_falsey } end describe '#verify with corrupted signature' do subject { sign_response } it "returns falsey" do allow(subject).to receive(:signature).and_return("bad signature") expect(subject.verify(app_id, public_key_pem)).to be_falsey end end end u2f-0.2.1/spec/lib/sign_request_spec.rb0000644000175600017570000000114412730505562017023 0ustar pravipravirequire 'spec_helper' describe U2F::SignRequest do let(:app_id) { 'http://example.com' } let(:challenge) { 'fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g' } let(:key_handle) do 'CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w==' end let(:sign_request) do U2F::SignRequest.new(key_handle, challenge, app_id) end describe '#to_json' do subject { sign_request.to_json } it do is_expected.to match_json_expression( version: String, appId: String, challenge: String, keyHandle: String ) end end endu2f-0.2.1/spec/lib/register_response_spec.rb0000644000175600017570000000506412730505562020062 0ustar pravipravirequire 'spec_helper.rb' describe U2F::RegisterResponse do let(:app_id) { 'http://demo.example.com' } let(:challenge) { U2F.urlsafe_encode64(SecureRandom.random_bytes(32)) } let(:device) { U2F::FakeU2F.new(app_id) } let(:key_handle) { U2F.urlsafe_encode64(device.key_handle_raw) } let(:public_key) { Base64.strict_encode64(device.origin_public_key_raw) } let(:certificate) { Base64.strict_encode64(device.cert_raw) } let(:registration_data_json) { device.register_response(challenge) } let(:registration_data_json_without_padding) do device.register_response(challenge).gsub(" ", "") end let(:error_response) { device.register_response(challenge, error = true) } let(:registration_request) { U2F::RegisterRequest.new(challenge, app_id) } let(:register_response) do U2F::RegisterResponse.load_from_json(registration_data_json) end context 'with error response' do let(:registration_data_json) { error_response } it 'raises RegistrationError with code' do expect { register_response }.to raise_error(U2F::RegistrationError) do |error| expect(error.code).to eq(4) end end end context 'with unpadded response' do let(:registration_data_json) { registration_data_json_without_padding } it 'does not raise "invalid base64" exception' do expect { register_response }.not_to raise_error end end describe '#certificate' do subject { register_response.certificate } it { is_expected.to eq certificate } end describe '#client_data' do context 'challenge' do subject { register_response.client_data.challenge } it { is_expected.to eq challenge } end end describe '#key_handle' do subject { register_response.key_handle } it { is_expected.to eq key_handle } end describe '#key_handle_length' do subject { register_response.key_handle_length } it { is_expected.to eq U2F.urlsafe_decode64(key_handle).length } end describe '#public_key' do subject { register_response.public_key } it { is_expected.to eq public_key } end describe '#verify' do subject { register_response.verify(app_id) } it { is_expected.to be_truthy } end describe '#verify with wrong app_id' do subject { register_response.verify("other app") } it { is_expected.to be_falsey } end describe '#verify with corrupted signature' do subject { register_response } it "returns falsey" do allow(subject).to receive(:signature).and_return("bad signature") expect(subject.verify(app_id)).to be_falsey end end end u2f-0.2.1/LICENSE0000644000175600017570000000211312730505562012256 0ustar pravipraviThe MIT License Copyright (c) 2014 by Johan Brissmyr and Sebastian Wallin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. u2f-0.2.1/u2f.gemspec0000644000175600017570000000531212730505562013316 0ustar pravipravi######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: u2f 0.2.1 ruby lib Gem::Specification.new do |s| s.name = "u2f" s.version = "0.2.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.require_paths = ["lib"] s.authors = ["Johan Brissmyr", "Sebastian Wallin"] s.date = "2015-10-06" s.description = "Library for handling registration and authentication of U2F devices" s.email = ["brissmyr@gmail.com", "sebastian.wallin@gmail.com"] s.files = ["LICENSE", "README.md", "lib/u2f.rb", "lib/u2f/client_data.rb", "lib/u2f/errors.rb", "lib/u2f/fake_u2f.rb", "lib/u2f/register_request.rb", "lib/u2f/register_response.rb", "lib/u2f/registration.rb", "lib/u2f/request_base.rb", "lib/u2f/sign_request.rb", "lib/u2f/sign_response.rb", "lib/u2f/u2f.rb", "lib/version.rb", "spec/lib/client_data_spec.rb", "spec/lib/register_request_spec.rb", "spec/lib/register_response_spec.rb", "spec/lib/sign_request_spec.rb", "spec/lib/sign_response_spec.rb", "spec/lib/u2f_spec.rb", "spec/spec_helper.rb"] s.homepage = "https://github.com/castle/ruby-u2f" s.licenses = ["MIT"] s.rubygems_version = "2.5.1" s.summary = "U2F library" s.test_files = ["spec/lib/client_data_spec.rb", "spec/lib/register_request_spec.rb", "spec/lib/register_response_spec.rb", "spec/lib/sign_request_spec.rb", "spec/lib/sign_response_spec.rb", "spec/lib/u2f_spec.rb", "spec/spec_helper.rb"] if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q, ["~> 0.7.2"]) s.add_development_dependency(%q, ["~> 0.8.3"]) s.add_development_dependency(%q, ["~> 10.3.2"]) s.add_development_dependency(%q, ["~> 3.1.0"]) s.add_development_dependency(%q, ["~> 0.27.1"]) s.add_development_dependency(%q, ["~> 0.9.1"]) else s.add_dependency(%q, ["~> 0.7.2"]) s.add_dependency(%q, ["~> 0.8.3"]) s.add_dependency(%q, ["~> 10.3.2"]) s.add_dependency(%q, ["~> 3.1.0"]) s.add_dependency(%q, ["~> 0.27.1"]) s.add_dependency(%q, ["~> 0.9.1"]) end else s.add_dependency(%q, ["~> 0.7.2"]) s.add_dependency(%q, ["~> 0.8.3"]) s.add_dependency(%q, ["~> 10.3.2"]) s.add_dependency(%q, ["~> 3.1.0"]) s.add_dependency(%q, ["~> 0.27.1"]) s.add_dependency(%q, ["~> 0.9.1"]) end end u2f-0.2.1/README.md0000644000175600017570000001573312730505562012544 0ustar pravipravi# Ruby U2F [![Gem Version](https://badge.fury.io/rb/u2f.png)](http://badge.fury.io/rb/u2f) [![Dependency Status](https://gemnasium.com/castle/ruby-u2f.svg)](https://gemnasium.com/castle/ruby-u2f) [![security](https://hakiri.io/github/castle/ruby-u2f/master.svg)](https://hakiri.io/github/castle/ruby-u2f/master) [![Build Status](https://travis-ci.org/castle/ruby-u2f.png)](https://travis-ci.org/castle/ruby-u2f) [![Code Climate](https://codeclimate.com/github/castle/ruby-u2f/badges/gpa.svg)](https://codeclimate.com/github/castle/ruby-u2f) [![Coverage Status](https://img.shields.io/coveralls/castle/ruby-u2f.svg)](https://coveralls.io/r/castle/ruby-u2f) Provides functionality for working with the server side aspects of the U2F 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). ## What is U2F? 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/). ## Working example application Check out the [example](https://github.com/castle/ruby-u2f/tree/master/example) directory for a fully working Padrino server demonstrating U2F. 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/). You'll need Google Chrome 41 or later to use U2F. ## Installation Add the `u2f` gem to your `Gemfile` ```ruby gem 'u2f' ``` ## Usage The U2F library has two major tasks: - **Register** new devices. - **Authenticate** previously registered devices. Each task starts by generating a challenge on the server, which is rendered to a web view, read by the browser API:s 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. You'll need an instance of `U2F:U2F`, which is conveniently placed in an [instance method](https://github.com/castle/ruby-u2f/blob/master/example/app/helpers/helpers.rb) on the controller. The initializer takes an **App ID** as argument. ```ruby def u2f @u2f ||= U2F::U2F.new(request.base_url) end ``` **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. ### Registration Generate the requests which will be sent to the U2F device. ```ruby # registrations_controller.rb def new # Generate one for each version of U2F, currently only `U2F_V2` @registration_requests = u2f.registration_requests # Store challenges. We need them for the verification step session[:challenges] = @registration_requests.map(&:challenge) # Fetch existing Registrations from your db and generate SignRequests key_handles = Registration.map(&:key_handle) @sign_requests = u2f.authentication_requests(key_handles) render 'registrations/new' end ``` Render a form that will be automatically posted when the U2F device reponds. ```html
``` ```javascript // render requests from server into Javascript format var registerRequests = <%= @registration_requests.as_json.to_json.html_safe %>; var signRequests = <%= @sign_requests.as_json.to_json.html_safe %>; u2f.register(registerRequests, signRequests, function(registerResponse) { var form, reg; if (registerResponse.errorCode) { return alert("Registration error: " + registerResponse.errorCode); } form = document.forms[0]; response = document.querySelector('[name=response]'); response.value = JSON.stringify(registerResponse); form.submit(); }); ``` Catch the response on your server, verify it, and store a reference to it in your database. ```ruby # registrations_controller.rb def create response = U2F::RegisterResponse.load_from_json(params[:response]) reg = begin u2f.register!(session[:challenges], response) rescue U2F::Error => e return "Unable to register: <%= e.class.name %>" ensure session.delete(:challenges) end # save a reference to your database Registration.create!(certificate: reg.certificate, key_handle: reg.key_handle, public_key: reg.public_key, counter: reg.counter) 'Registered!' end ``` ### Authentication Generate the requests which will be sent to the U2F device. ```ruby # authentications_controller.rb def new # Fetch existing Registrations from your db key_handles = Registration.map(&:key_handle) return 'Need to register first' if key_handles.empty? # Generate SignRequests @sign_requests = u2f.authentication_requests(key_handles) # Store challenges. We need them for the verification step session[:challenges] = @sign_requests.map(&:challenge) render 'authentications/new' end ``` Render a form that will be automatically posted when the U2F device reponds. ```html
``` ```javascript // render requests from server into Javascript format var signRequests = <%= @sign_requests.as_json.to_json.html_safe %>; u2f.sign(signRequests, function(signResponse) { var form, reg; if (signResponse.errorCode) { return alert("Authentication error: " + signResponse.errorCode); } form = document.forms[0]; response = document.querySelector('[name=response]'); response.value = JSON.stringify(signResponse); form.submit(); }); ``` Catch the response on your server, verify it, and bump the counter in your database reference. ```ruby # authentications_controller.rb def create response = U2F::SignResponse.load_from_json(params[:response]) registration = Registration.first(key_handle: response.key_handle) return 'Need to register first' unless registration begin u2f.authenticate!(session[:challenges], response, Base64.decode64(registration.public_key), registration.counter) rescue U2F::Error => e return "Unable to authenticate: <%= e.class.name %>" ensure session.delete(:challenges) end registration.update(counter: response.counter) 'Authenticated!' end ``` ## License MIT License. Copyright (c) 2015 by Johan Brissmyr and Sebastian Wallin u2f-0.2.1/lib/0000755000175600017570000000000012730505562012022 5ustar pravipraviu2f-0.2.1/lib/version.rb0000644000175600017570000000004312730505562014031 0ustar pravipravimodule U2F VERSION = "0.2.1" end u2f-0.2.1/lib/u2f.rb0000644000175600017570000000060112730505562013040 0ustar pravipravirequire 'base64' require 'json' require 'openssl' require 'securerandom' require 'u2f/client_data' require 'u2f/errors' require 'u2f/request_base' require 'u2f/register_request' require 'u2f/register_response' require 'u2f/registration' require 'u2f/sign_request' require 'u2f/sign_response' require 'u2f/fake_u2f' require 'u2f/u2f' module U2F DIGEST = OpenSSL::Digest::SHA256 end u2f-0.2.1/lib/u2f/0000755000175600017570000000000012730505562012516 5ustar pravipraviu2f-0.2.1/lib/u2f/errors.rb0000644000175600017570000000164012730505562014360 0ustar pravipravimodule U2F class Error < StandardError;end class UnmatchedChallengeError < Error; end class ClientDataTypeError < Error; end class PublicKeyDecodeError < Error; end class AttestationDecodeError < Error; end class AttestationVerificationError < Error; end class AttestationSignatureError < Error; end class NoMatchingRequestError < Error; end class NoMatchingRegistrationError < Error; end class CounterTooLowError < Error; end class AuthenticationFailedError < Error; end class UserNotPresentError < Error;end class RegistrationError < Error CODES = { 1 => "OTHER_ERROR", 2 => "BAD_REQUEST", 3 => "CONFIGURATION_UNSUPPORTED", 4 => "DEVICE_INELIGIBLE", 5 => "TIMEOUT" } attr_reader :code def initialize(options = {}) @code = options[:code] message = options[:message] || "Token returned #{CODES[code]}" super(message) end end end u2f-0.2.1/lib/u2f/sign_response.rb0000644000175600017570000000276412730505562015732 0ustar pravipravimodule U2F class SignResponse attr_accessor :client_data, :client_data_json, :key_handle, :signature_data def self.load_from_json(json) data = ::JSON.parse(json) instance = new instance.client_data_json = ::U2F.urlsafe_decode64(data['clientData']) instance.client_data = ClientData.load_from_json(instance.client_data_json) instance.key_handle = data['keyHandle'] instance.signature_data = ::U2F.urlsafe_decode64(data['signatureData']) instance end ## # Counter value that the U2F token increments every time it performs an # authentication operation def counter signature_data.byteslice(1, 4).unpack('N').first end ## # signature is to be verified using the public key obtained during # registration. def signature signature_data.byteslice(5..-1) end ## # If user presence was verified def user_present? signature_data.byteslice(0).unpack('C').first == 1 end ## # Verifies the response against an app id and the public key of the # registered device def verify(app_id, public_key_pem) data = [ ::U2F::DIGEST.digest(app_id), signature_data.byteslice(0, 5), ::U2F::DIGEST.digest(client_data_json) ].join public_key = OpenSSL::PKey.read(public_key_pem) begin public_key.verify(::U2F::DIGEST.new, signature, data) rescue OpenSSL::PKey::PKeyError false end end end end u2f-0.2.1/lib/u2f/registration.rb0000644000175600017570000000057412730505562015563 0ustar pravipravimodule U2F ## # A representation of a registered U2F device class Registration attr_accessor :key_handle, :public_key, :certificate, :counter def initialize(key_handle, public_key, certificate) @key_handle = key_handle @public_key = public_key @certificate = certificate end def counter @counter.nil? ? 0 : @counter end end endu2f-0.2.1/lib/u2f/sign_request.rb0000644000175600017570000000047512730505562015561 0ustar pravipravimodule U2F class SignRequest include RequestBase attr_accessor :key_handle def initialize(key_handle, challenge, app_id) @key_handle = key_handle @challenge = challenge @app_id = app_id end def as_json(options = {}) super.merge(keyHandle: key_handle) end end end u2f-0.2.1/lib/u2f/client_data.rb0000644000175600017570000000136312730505562015315 0ustar pravipravimodule U2F ## # A representation of ClientData, chapter 7 # http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf class ClientData REGISTRATION_TYP = "navigator.id.finishEnrollment".freeze AUTHENTICATION_TYP = "navigator.id.getAssertion".freeze attr_accessor :typ, :challenge, :origin alias_method :type, :typ def registration? typ == REGISTRATION_TYP end def authentication? typ == AUTHENTICATION_TYP end def self.load_from_json(json) client_data = ::JSON.parse(json) instance = new instance.typ = client_data['typ'] instance.challenge = client_data['challenge'] instance.origin = client_data['origin'] instance end end end u2f-0.2.1/lib/u2f/request_base.rb0000644000175600017570000000053512730505562015530 0ustar pravipravimodule U2F module RequestBase attr_accessor :version, :challenge, :app_id def as_json(options = {}) { version: version, challenge: challenge, appId: app_id } end def to_json(options = {}) ::JSON.pretty_generate(as_json, options) end def version 'U2F_V2' end end end u2f-0.2.1/lib/u2f/fake_u2f.rb0000644000175600017570000001313612730505562014531 0ustar pravipraviclass U2F::FakeU2F CURVE_NAME = "prime256v1".freeze attr_accessor :app_id, :counter, :key_handle_raw, :cert_subject # Initialize a new FakeU2F device for use in tests. # # app_id - The appId/origin this is being tested against. # options - A Hash of optional parameters (optional). # :counter - The initial counter for this device. # :key_handle - The raw key-handle this device should use. # :cert_subject - The subject field for the certificate generated # for this device. # # Returns nothing. def initialize(app_id, options = {}) @app_id = app_id @counter = options.fetch(:counter, 0) @key_handle_raw = options.fetch(:key_handle, SecureRandom.random_bytes(32)) @cert_subject = options.fetch(:cert_subject, "/CN=U2FTest") end # A registerResponse hash as returned by the u2f.register JavaScript API. # # challenge - The challenge to sign. # error - Boolean. Whether to return an error response (optional). # # Returns a JSON encoded Hash String. def register_response(challenge, error = false) if error JSON.dump(:errorCode => 4) else client_data_json = client_data(U2F::ClientData::REGISTRATION_TYP, challenge) JSON.dump( :registrationData => reg_registration_data(client_data_json), :clientData => U2F.urlsafe_encode64(client_data_json) ) end end # A SignResponse hash as returned by the u2f.sign JavaScript API. # # challenge - The challenge to sign. # # Returns a JSON encoded Hash String. def sign_response(challenge) client_data_json = client_data(U2F::ClientData::AUTHENTICATION_TYP, challenge) JSON.dump( :clientData => U2F.urlsafe_encode64(client_data_json), :keyHandle => U2F.urlsafe_encode64(key_handle_raw), :signatureData => auth_signature_data(client_data_json) ) end # The appId specific public key as returned in the registrationData field of # a RegisterResponse Hash. # # Returns a binary formatted EC public key String. def origin_public_key_raw [origin_key.public_key.to_bn.to_s(16)].pack('H*') end # The raw device attestation certificate as returned in the registrationData # field of a RegisterResponse Hash. # # Returns a DER formatted certificate String. def cert_raw cert.to_der end private # The registrationData field returns in a RegisterResponse Hash. # # client_data_json - The JSON encoded clientData String. # # Returns a url-safe base64 encoded binary String. def reg_registration_data(client_data_json) U2F.urlsafe_encode64( [ 5, origin_public_key_raw, key_handle_raw.bytesize, key_handle_raw, cert_raw, reg_signature(client_data_json) ].pack("CA65CA#{key_handle_raw.bytesize}A#{cert_raw.bytesize}A*") ) end # The signature field of a registrationData field of a RegisterResponse. # # client_data_json - The JSON encoded clientData String. # # Returns an ECDSA signature String. def reg_signature(client_data_json) payload = [ "\x00", U2F::DIGEST.digest(app_id), U2F::DIGEST.digest(client_data_json), key_handle_raw, origin_public_key_raw ].join cert_key.sign(U2F::DIGEST.new, payload) end # The signatureData field of a SignResponse Hash. # # client_data_json - The JSON encoded clientData String. # # Returns a url-safe base64 encoded binary String. def auth_signature_data(client_data_json) ::U2F.urlsafe_encode64( [ 1, # User present self.counter += 1, auth_signature(client_data_json) ].pack("CNA*") ) end # The signature field of a signatureData field of a SignResponse Hash. # # client_data_json - The JSON encoded clientData String. # # Returns an ECDSA signature String. def auth_signature(client_data_json) data = [ U2F::DIGEST.digest(app_id), 1, # User present counter, U2F::DIGEST.digest(client_data_json) ].pack("A32CNA32") origin_key.sign(U2F::DIGEST.new, data) end # The clientData hash as returned by registration and authentication # responses. # # typ - The String value for the 'typ' field. # challenge - The String url-safe base64 encoded challenge parameter. # # Returns a JSON encoded Hash String. def client_data(typ, challenge) JSON.dump( :challenge => challenge, :origin => app_id, :typ => typ ) end # The appId-specific public/private key. # # Returns a OpenSSL::PKey::EC instance. def origin_key @origin_key ||= generate_ec_key end # The self-signed device attestation certificate. # # Returns a OpenSSL::X509::Certificate instance. def cert @cert ||= OpenSSL::X509::Certificate.new.tap do |c| c.subject = c.issuer = OpenSSL::X509::Name.parse(cert_subject) c.not_before = Time.now c.not_after = Time.now + 365 * 24 * 60 * 60 c.public_key = cert_key c.serial = 0x1 c.version = 0x0 c.sign cert_key, U2F::DIGEST.new end end # The public key used for signing the device certificate. # # Returns a OpenSSL::PKey::EC instance. def cert_key @cert_key ||= generate_ec_key end # Generate an eliptic curve public/private key. # # Returns a OpenSSL::PKey::EC instance. def generate_ec_key OpenSSL::PKey::EC.new().tap do |ec| ec.group = OpenSSL::PKey::EC::Group.new(CURVE_NAME) ec.generate_key # https://bugs.ruby-lang.org/issues/8177 ec.define_singleton_method(:private?) { private_key? } ec.define_singleton_method(:public?) { public_key? } end end end u2f-0.2.1/lib/u2f/u2f.rb0000644000175600017570000001337412730505562013547 0ustar pravipravimodule U2F class U2F attr_accessor :app_id ## # * *Args*: # - +app_id+:: An application (facet) ID string # def initialize(app_id) @app_id = app_id end ## # Generate data to be sent to the U2F device before authenticating # # * *Args*: # - +key_handles+:: +Array+ of previously registered U2F key handles # # * *Returns*: # - An +Array+ of +SignRequest+ objects # def authentication_requests(key_handles) key_handles = [key_handles] unless key_handles.is_a? Array key_handles.map do |key_handle| SignRequest.new(key_handle, challenge, app_id) end end ## # Authenticate a response from the U2F device # # * *Args*: # - +challenges+:: +Array+ of challenge strings # - +response+:: Response from the U2F device as a +SignResponse+ object # - +registration_public_key+:: Public key of the registered U2F device as binary string # - +registration_counter+:: +Integer+ with the current counter value of the registered device. # # * *Raises*: # - +NoMatchingRequestError+:: if the challenge in the response doesn't match any of the provided ones. # - +ClientDataTypeError+:: if the response is of the wrong type # - +AuthenticationFailedError+:: if the authentication failed # - +UserNotPresentError+:: if the user wasn't present during the authentication # - +CounterTooLowError+:: if there is a counter mismatch between the registered one and the one in the response. # def authenticate!(challenges, response, registration_public_key, registration_counter) # Handle both single and Array input challenges = [challenges] unless challenges.is_a? Array # TODO: check that it's the correct key_handle as well unless challenges.include?(response.client_data.challenge) fail NoMatchingRequestError end fail ClientDataTypeError unless response.client_data.authentication? pem = U2F.public_key_pem(registration_public_key) fail AuthenticationFailedError unless response.verify(app_id, pem) fail UserNotPresentError unless response.user_present? unless response.counter > registration_counter unless response.counter == 0 && registration_counter == 0 fail CounterTooLowError end end end ## # Generates a 32 byte long random U2F challenge # # * *Returns*: # - Base64 urlsafe encoded challenge # def challenge ::U2F.urlsafe_encode64(SecureRandom.random_bytes(32)) end ## # Generate data to be used when registering a U2F device # # * *Returns*: # - An +Array+ of +RegisterRequest+ objects # def registration_requests # TODO: generate a request for each supported version [RegisterRequest.new(challenge, @app_id)] end ## # Authenticate the response from the U2F device when registering # # * *Args*: # - +challenges+:: +Array+ of challenge strings # - +response+:: Response of the U2F device as a +RegisterResponse+ object # # * *Returns*: # - A +Registration+ object # # * *Raises*: # - +UnmatchedChallengeError+:: if the challenge in the response doesn't match any of the provided ones. # - +ClientDataTypeError+:: if the response is of the wrong type # - +AttestationSignatureError+:: if the registration failed # def register!(challenges, response) challenges = [challenges] unless challenges.is_a? Array challenge = challenges.detect do |chg| chg == response.client_data.challenge end fail UnmatchedChallengeError unless challenge fail ClientDataTypeError unless response.client_data.registration? # Validate public key U2F.public_key_pem(response.public_key_raw) # TODO: # unless U2F.validate_certificate(response.certificate_raw) # fail AttestationVerificationError # end fail AttestationSignatureError unless response.verify(app_id) registration = Registration.new( response.key_handle, response.public_key, response.certificate ) registration end ## # Convert a binary public key to PEM format # * *Args*: # - +key+:: Binary public key # # * *Returns*: # - A base64 encoded public key +String+ in PEM format # # * *Raises*: # - +PublicKeyDecodeError+:: if the +key+ argument is incorrect # def self.public_key_pem(key) fail PublicKeyDecodeError unless key.bytesize == 65 && key.byteslice(0) == "\x04" # http://tools.ietf.org/html/rfc5480 der = OpenSSL::ASN1::Sequence([ OpenSSL::ASN1::Sequence([ OpenSSL::ASN1::ObjectId('1.2.840.10045.2.1'), # id-ecPublicKey OpenSSL::ASN1::ObjectId('1.2.840.10045.3.1.7') # secp256r1 ]), OpenSSL::ASN1::BitString(key) ]).to_der pem = "-----BEGIN PUBLIC KEY-----\r\n" + Base64.strict_encode64(der).scan(/.{1,64}/).join("\r\n") + "\r\n-----END PUBLIC KEY-----" pem end # def self.validate_certificate(_certificate_raw) # TODO # cacert = OpenSSL::X509::Certificate.new() # cert = OpenSSL::X509::Certificate.new(certificate_raw) # cert.verify(cacert.public_key) # end end ## # Variant of Base64::urlsafe_decode64 which adds padding if necessary # def self.urlsafe_decode64(string) string = case string.length % 4 when 2 then string + '==' when 3 then string + '=' else string end Base64.urlsafe_decode64(string) end ## # Variant of Base64::urlsafe_encode64 which removes padding # def self.urlsafe_encode64(string) Base64.urlsafe_encode64(string).delete('=') end end u2f-0.2.1/lib/u2f/register_response.rb0000644000175600017570000000611212730505562016605 0ustar pravipravimodule U2F ## # Representation of a U2F registration response. # See chapter 4.3: # http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf class RegisterResponse attr_accessor :client_data, :client_data_json, :registration_data_raw PUBLIC_KEY_OFFSET = 1 PUBLIC_KEY_LENGTH = 65 KEY_HANDLE_LENGTH_LENGTH = 1 KEY_HANDLE_LENGTH_OFFSET = PUBLIC_KEY_OFFSET + PUBLIC_KEY_LENGTH KEY_HANDLE_OFFSET = KEY_HANDLE_LENGTH_OFFSET + KEY_HANDLE_LENGTH_LENGTH def self.load_from_json(json) # TODO: validate data = JSON.parse(json) if data['errorCode'] && data['errorCode'] > 0 fail RegistrationError, :code => data['errorCode'] end instance = new instance.client_data_json = ::U2F.urlsafe_decode64(data['clientData']) instance.client_data = ClientData.load_from_json(instance.client_data_json) instance.registration_data_raw = ::U2F.urlsafe_decode64(data['registrationData']) instance end ## # The attestation certificate in Base64 encoded X.509 DER format def certificate Base64.strict_encode64(parsed_certificate.to_der) end ## # The parsed attestation certificate def parsed_certificate OpenSSL::X509::Certificate.new(certificate_bytes) end ## # Length of the attestation certificate def certificate_length parsed_certificate.to_der.bytesize end ## # Returns the key handle from registration data, URL safe base64 encoded def key_handle ::U2F.urlsafe_encode64(key_handle_raw) end def key_handle_raw registration_data_raw.byteslice(KEY_HANDLE_OFFSET, key_handle_length) end ## # Returns the length of the key handle, extracted from the registration data def key_handle_length registration_data_raw.byteslice(KEY_HANDLE_LENGTH_OFFSET).unpack('C').first end ## # Returns the public key, extracted from the registration data def public_key # Base64 encode without linefeeds Base64.strict_encode64(public_key_raw) end def public_key_raw registration_data_raw.byteslice(PUBLIC_KEY_OFFSET, PUBLIC_KEY_LENGTH) end ## # Returns the signature, extracted from the registration data def signature registration_data_raw.byteslice( (KEY_HANDLE_OFFSET + key_handle_length + certificate_length)..-1) end ## # Verifies the registration data against the app id def verify(app_id) # Chapter 4.3 in # http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf data = [ "\x00", ::U2F::DIGEST.digest(app_id), ::U2F::DIGEST.digest(client_data_json), key_handle_raw, public_key_raw ].join begin parsed_certificate.public_key.verify(::U2F::DIGEST.new, signature, data) rescue OpenSSL::PKey::PKeyError false end end private def certificate_bytes base_offset = KEY_HANDLE_OFFSET + key_handle_length registration_data_raw.byteslice(base_offset..-1) end end end u2f-0.2.1/lib/u2f/register_request.rb0000644000175600017570000000024712730505562016442 0ustar pravipravimodule U2F class RegisterRequest include RequestBase def initialize(challenge, app_id) @challenge = challenge @app_id = app_id end end end