u2f-1.0.0/0000755000004100000410000000000013345260241012234 5ustar www-datawww-datau2f-1.0.0/README.md0000644000004100000410000001573213345260241013523 0ustar www-datawww-data# 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 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. 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-1.0.0/spec/0000755000004100000410000000000013345260241013166 5ustar www-datawww-datau2f-1.0.0/spec/lib/0000755000004100000410000000000013345260241013734 5ustar www-datawww-datau2f-1.0.0/spec/lib/register_response_spec.rb0000644000004100000410000000505413345260241021041 0ustar www-datawww-datarequire '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) } 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-1.0.0/spec/lib/u2f_spec.rb0000644000004100000410000001023513345260241015770 0ustar www-datawww-data 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) 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-1.0.0/spec/lib/client_data_spec.rb0000644000004100000410000000166013345260241017545 0ustar www-datawww-datarequire '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-1.0.0/spec/lib/sign_response_spec.rb0000644000004100000410000000230613345260241020152 0ustar www-datawww-datarequire '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-1.0.0/spec/lib/register_request_spec.rb0000644000004100000410000000060513345260241020670 0ustar www-datawww-datarequire 'spec_helper' describe U2F::RegisterRequest do let(:challenge) { 'fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g' } let(:sign_request) do U2F::RegisterRequest.new(challenge) end describe '#to_json' do subject { sign_request.to_json } it do is_expected.to match_json_expression( version: String, challenge: String ) end end end u2f-1.0.0/spec/lib/sign_request_spec.rb0000644000004100000410000000066413345260241020011 0ustar www-datawww-datarequire 'spec_helper' describe U2F::SignRequest do let(:key_handle) do 'CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w==' end let(:sign_request) do U2F::SignRequest.new(key_handle) end describe '#to_json' do subject { sign_request.to_json } it do is_expected.to match_json_expression( version: String, keyHandle: String ) end end end u2f-1.0.0/spec/spec_helper.rb0000644000004100000410000000041313345260241016002 0ustar www-datawww-datarequire '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-1.0.0/LICENSE0000644000004100000410000000211313345260241013236 0ustar www-datawww-dataThe 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-1.0.0/lib/0000755000004100000410000000000013345260241013002 5ustar www-datawww-datau2f-1.0.0/lib/version.rb0000644000004100000410000000004313345260241015011 0ustar www-datawww-datamodule U2F VERSION = '1.0.0' end u2f-1.0.0/lib/u2f/0000755000004100000410000000000013345260241013476 5ustar www-datawww-datau2f-1.0.0/lib/u2f/fake_u2f.rb0000644000004100000410000001313613345260241015511 0ustar www-datawww-dataclass 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-1.0.0/lib/u2f/register_request.rb0000644000004100000410000000042413345260241017417 0ustar www-datawww-datamodule U2F class RegisterRequest include RequestBase attr_accessor :challenge def initialize(challenge) @challenge = challenge end def as_json(options = {}) { version: version, challenge: challenge } end end end u2f-1.0.0/lib/u2f/errors.rb0000644000004100000410000000164013345260241015340 0ustar www-datawww-datamodule 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-1.0.0/lib/u2f/request_base.rb0000644000004100000410000000030313345260241016501 0ustar www-datawww-datamodule U2F module RequestBase attr_accessor :version def to_json(options = {}) ::JSON.pretty_generate(as_json, options) end def version 'U2F_V2' end end end u2f-1.0.0/lib/u2f/sign_request.rb0000644000004100000410000000042513345260241016534 0ustar www-datawww-datamodule U2F class SignRequest include RequestBase attr_accessor :key_handle def initialize(key_handle) @key_handle = key_handle end def as_json(options = {}) { version: version, keyHandle: key_handle } end end end u2f-1.0.0/lib/u2f/u2f.rb0000644000004100000410000001315613345260241014525 0ustar www-datawww-datamodule 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) 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!(challenge, response, registration_public_key, registration_counter) # TODO: check that it's the correct key_handle as well unless challenge == 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)] 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-1.0.0/lib/u2f/register_response.rb0000644000004100000410000000611213345260241017565 0ustar www-datawww-datamodule 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-1.0.0/lib/u2f/registration.rb0000644000004100000410000000057413345260241016543 0ustar www-datawww-datamodule 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-1.0.0/lib/u2f/sign_response.rb0000644000004100000410000000333313345260241016703 0ustar www-datawww-datamodule 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 # Bit 0 being set to 1 indicates that the user is present. A different value # of Bit 0, as well as Bits 1 through 7, are reserved for future use. USER_PRESENCE_MASK = 0b00000001 ## # If user presence was verified def user_present? byte = signature_data.byteslice(0).unpack('C').first byte & USER_PRESENCE_MASK == 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-1.0.0/lib/u2f/client_data.rb0000644000004100000410000000136313345260241016275 0ustar www-datawww-datamodule 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-1.0.0/lib/u2f.rb0000644000004100000410000000060113345260241014020 0ustar www-datawww-datarequire '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-1.0.0/u2f.gemspec0000644000004100000410000000624013345260241014277 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: u2f 1.0.0 ruby lib Gem::Specification.new do |s| s.name = "u2f".freeze s.version = "1.0.0" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Johan Brissmyr".freeze, "Sebastian Wallin".freeze] s.date = "2017-03-05" s.description = "Library for handling registration and authentication of U2F devices".freeze s.email = ["brissmyr@gmail.com".freeze, "sebastian.wallin@gmail.com".freeze] s.files = ["LICENSE".freeze, "README.md".freeze, "lib/u2f.rb".freeze, "lib/u2f/client_data.rb".freeze, "lib/u2f/errors.rb".freeze, "lib/u2f/fake_u2f.rb".freeze, "lib/u2f/register_request.rb".freeze, "lib/u2f/register_response.rb".freeze, "lib/u2f/registration.rb".freeze, "lib/u2f/request_base.rb".freeze, "lib/u2f/sign_request.rb".freeze, "lib/u2f/sign_response.rb".freeze, "lib/u2f/u2f.rb".freeze, "lib/version.rb".freeze, "spec/lib/client_data_spec.rb".freeze, "spec/lib/register_request_spec.rb".freeze, "spec/lib/register_response_spec.rb".freeze, "spec/lib/sign_request_spec.rb".freeze, "spec/lib/sign_response_spec.rb".freeze, "spec/lib/u2f_spec.rb".freeze, "spec/spec_helper.rb".freeze] s.homepage = "https://github.com/castle/ruby-u2f".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.0.0".freeze) s.rubygems_version = "2.5.2.1".freeze s.summary = "U2F library".freeze s.test_files = ["spec/lib/client_data_spec.rb".freeze, "spec/lib/register_request_spec.rb".freeze, "spec/lib/register_response_spec.rb".freeze, "spec/lib/sign_request_spec.rb".freeze, "spec/lib/sign_response_spec.rb".freeze, "spec/lib/u2f_spec.rb".freeze, "spec/spec_helper.rb".freeze] 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.freeze, ["~> 0.8.10"]) s.add_development_dependency(%q.freeze, ["~> 0.8.3"]) s.add_development_dependency(%q.freeze, ["~> 10.3"]) s.add_development_dependency(%q.freeze, ["~> 3.1"]) s.add_development_dependency(%q.freeze, ["~> 0.27.1"]) s.add_development_dependency(%q.freeze, ["~> 0.11.1"]) else s.add_dependency(%q.freeze, ["~> 0.8.10"]) s.add_dependency(%q.freeze, ["~> 0.8.3"]) s.add_dependency(%q.freeze, ["~> 10.3"]) s.add_dependency(%q.freeze, ["~> 3.1"]) s.add_dependency(%q.freeze, ["~> 0.27.1"]) s.add_dependency(%q.freeze, ["~> 0.11.1"]) end else s.add_dependency(%q.freeze, ["~> 0.8.10"]) s.add_dependency(%q.freeze, ["~> 0.8.3"]) s.add_dependency(%q.freeze, ["~> 10.3"]) s.add_dependency(%q.freeze, ["~> 3.1"]) s.add_dependency(%q.freeze, ["~> 0.27.1"]) s.add_dependency(%q.freeze, ["~> 0.11.1"]) end end