u2f 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 75a50469828893782345c5b90dfc2bf0272fa3c2
4
- data.tar.gz: 466e51009f4f2c670302d67930fc66e33c80c49d
3
+ metadata.gz: 047cac5bb30fd3ec7ab0d032ae465e6d9e9cadc0
4
+ data.tar.gz: 041fe8a755ec15ddd949523f61ed0863d646aae5
5
5
  SHA512:
6
- metadata.gz: a412287c1b11d1c85d966a3a7e68dfb7da43c7df73da08f67f06b1ace63dc28bfde8a83fcb6a9d6c748cecf4243552986d0abce203718f317add402c171cb3b0
7
- data.tar.gz: e761c7a2a0a04de96a2008342fa6a6fc33d04fb743d327691df51a0f3dd13a12d86aa411ac1f88ae4eb99bbe3e7350099d4ff342663cce5846a520af2f0fef12
6
+ metadata.gz: 829483f4451930d2eb42bab1f95b68b8c96b128b9068079592074c4f4e3365bc920c9f5e0539a534091ce253a19f375ef7521de2e35ae206f57d2b0dded41fa5
7
+ data.tar.gz: 43719723ce28854b37528e208cf5bdfe8d2ed69479e088af45c41388b46c3ab4a2c34b17ba3b5b1c6812e77b5d355360216d3929e54320562604c59dea0fc735
data/README.md CHANGED
@@ -1,3 +1,206 @@
1
1
  # Ruby U2F
2
2
 
3
- Work in progress.
3
+ [![Gem Version](https://badge.fury.io/rb/u2f.png)](http://badge.fury.io/rb/u2f)
4
+ [![Build Status](https://travis-ci.org/userbin/ruby-u2f.png)](https://travis-ci.org/userbin/ruby-u2f)
5
+ [![Code Climate](https://codeclimate.com/github/userbin/ruby-u2f/badges/gpa.svg)](https://codeclimate.com/github/userbin/ruby-u2f)
6
+ [![Coverage Status](https://img.shields.io/coveralls/userbin/ruby-u2f.svg)](https://coveralls.io/r/userbin/ruby-u2f)
7
+ [![security](https://hakiri.io/github/userbin/ruby-u2f/master.svg)](https://hakiri.io/github/userbin/ruby-u2f/master)
8
+
9
+ Provides functionality for working with the server side aspects of the U2F
10
+ 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).
11
+
12
+ ## What is U2F?
13
+
14
+ 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/).
15
+
16
+ ## Working example application
17
+
18
+ Check out the [example](https://github.com/userbin/ruby-u2f/tree/master/example) directory for a fully working Padrino server demonstrating U2F.
19
+
20
+ ## Installation
21
+
22
+ Add the `u2f` gem to your `Gemfile`
23
+
24
+ ```ruby
25
+ gem 'u2f'
26
+ ```
27
+
28
+ Currently, you need Google Chrome and the [FIDO U2F extension](https://chrome.google.com/webstore/detail/fido-u2f-universal-2nd-fa/pfboblefjcgdjicmnffhdgionmgcdmne) to enable U2F. To access the extension’s JavaScript API, add the script to the `<head>` section.
29
+
30
+ ```html
31
+ <script src="chrome-extension://pfboblefjcgdjicmnffhdgionmgcdmne/u2f-api.js"></script>
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ The U2F library has two major tasks:
37
+
38
+ - **Register** new devices.
39
+ - **Authenticate** previously registered devices.
40
+
41
+ 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.
42
+
43
+ You'll need an instance of `U2F:U2F`, which is conveniently placed in an [instance method](https://github.com/userbin/ruby-u2f/blob/master/example/app/helpers/helpers.rb) on the controller. The initializer takes an **App ID** as argument.
44
+
45
+ ```ruby
46
+ def u2f
47
+ @u2f ||= U2F::U2F.new(request.base_url)
48
+ end
49
+ ```
50
+
51
+ **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.
52
+
53
+ ### Registration
54
+
55
+ Generate the requests which will be sent to the U2F device.
56
+
57
+ ```ruby
58
+ # registrations_controller.rb
59
+ def new
60
+ # Generate one for each version of U2F, currently only `U2F_V2`
61
+ @registration_requests = u2f.registration_requests
62
+
63
+ # Store challenges. We need them for the verification step
64
+ session[:challenges] = @registration_requests.map(&:challenge)
65
+
66
+ # Fetch existing Registrations from your db and generate SignRequests
67
+ key_handles = Registration.map(&:key_handle)
68
+ @sign_requests = u2f.authentication_requests(key_handles)
69
+
70
+ render 'registrations/new'
71
+ end
72
+ ```
73
+
74
+ Render a form that will be automatically posted when the U2F device reponds.
75
+
76
+ ```html
77
+ <!-- registrations/new.html -->
78
+ <form action="/registrations" method="post">
79
+ <input type="hidden" name="response">
80
+ </form>
81
+ ```
82
+
83
+ ```javascript
84
+ // render requests from server into Javascript format
85
+ var registerRequests = <%= @registration_requests.to_json.html_safe %>;
86
+ var signRequests = <%= @sign_requests.to_json.html_safe %>;
87
+
88
+ u2f.register(registerRequests, signRequests, function(registerResponse) {
89
+ var form, reg;
90
+
91
+ if (registerResponse.errorCode) {
92
+ return alert("Registration error: " + registerResponse.errorCode);
93
+ }
94
+
95
+ form = document.forms[0];
96
+ response = document.querySelector('[name=response]');
97
+
98
+ response.value = JSON.stringify(registerResponse);
99
+
100
+ form.submit();
101
+ });
102
+ ```
103
+
104
+ Catch the response on your server, verify it, and store a reference to it in your database.
105
+
106
+ ```ruby
107
+ # registrations_controller.rb
108
+ def create
109
+ response = U2F::RegisterResponse.load_from_json(params[:response])
110
+
111
+ reg = begin
112
+ u2f.register!(session[:challenges], response)
113
+ rescue U2F::Error => e
114
+ return "Unable to register: <%= e.class.name %>"
115
+ ensure
116
+ session.delete(:challenges)
117
+ end
118
+
119
+ # save a reference to your database
120
+ Registration.create!(certificate: reg.certificate,
121
+ key_handle: reg.key_handle,
122
+ public_key: reg.public_key,
123
+ counter: reg.counter)
124
+
125
+ 'Registered!'
126
+ end
127
+ ```
128
+
129
+ ### Authentication
130
+
131
+ Generate the requests which will be sent to the U2F device.
132
+
133
+ ```ruby
134
+ # authentications_controller.rb
135
+ def new
136
+ # Fetch existing Registrations from your db
137
+ key_handles = Registration.map(&:key_handle)
138
+ return 'Need to register first' if key_handles.empty?
139
+
140
+ # Generate SignRequests
141
+ @sign_requests = u2f.authentication_requests(key_handles)
142
+
143
+ # Store challenges. We need them for the verification step
144
+ session[:challenges] = @sign_requests.map(&:challenge)
145
+
146
+ render 'authentications/new'
147
+ end
148
+ ```
149
+
150
+ Render a form that will be automatically posted when the U2F device reponds.
151
+
152
+ ```html
153
+ <!-- registrations/new.html -->
154
+ <form action="/authentications" method="post">
155
+ <input type="hidden" name="response">
156
+ </form>
157
+ ```
158
+
159
+ ```javascript
160
+ // render requests from server into Javascript format
161
+ var signRequests = <%= @sign_requests.to_json.html_safe %>;
162
+
163
+ u2f.sign(signRequests, function(signResponse) {
164
+ var form, reg;
165
+
166
+ if (signResponse.errorCode) {
167
+ return alert("Authentication error: " + signResponse.errorCode);
168
+ }
169
+
170
+ form = document.forms[0];
171
+ response = document.querySelector('[name=response]');
172
+
173
+ response.value = JSON.stringify(signResponse);
174
+
175
+ form.submit();
176
+ });
177
+ ```
178
+
179
+ Catch the response on your server, verify it, and bump the counter in your database reference.
180
+
181
+ ```ruby
182
+ # authentications_controller.rb
183
+ def create
184
+ response = U2F::SignResponse.load_from_json(params[:response])
185
+
186
+ registration = Registration.first(key_handle: response.key_handle)
187
+ return 'Need to register first' unless registration
188
+
189
+ begin
190
+ u2f.authenticate!(session[:challenges], response,
191
+ registration.public_key, registration.counter)
192
+ rescue U2F::Error => e
193
+ return "Unable to authenticate: <%= e.class.name %>"
194
+ ensure
195
+ session.delete(:challenges)
196
+ end
197
+
198
+ registration.update(counter: response.counter)
199
+
200
+ 'Authenticated!'
201
+ end
202
+ ```
203
+
204
+ ## License
205
+
206
+ MIT License. Copyright (c) 2014 by Johan Brissmyr and Sebastian Wallin
data/lib/u2f.rb CHANGED
@@ -1,2 +1,17 @@
1
+ require 'base64'
2
+ require 'json'
3
+ require 'openssl'
4
+
5
+ require 'u2f/client_data'
6
+ require 'u2f/collection'
7
+ require 'u2f/errors'
8
+ require 'u2f/request_base'
9
+ require 'u2f/register_request'
10
+ require 'u2f/register_response'
11
+ require 'u2f/registration'
12
+ require 'u2f/sign_request'
13
+ require 'u2f/sign_response'
14
+ require 'u2f/u2f'
15
+
1
16
  module U2F
2
17
  end
@@ -0,0 +1,26 @@
1
+ module U2F
2
+ ##
3
+ # A representation of ClientData, chapter 7
4
+ # http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf
5
+ class ClientData
6
+ attr_accessor :typ, :challenge, :origin
7
+ alias_method :type, :typ
8
+
9
+ def registration?
10
+ typ == 'navigator.id.finishEnrollment'
11
+ end
12
+
13
+ def authentication?
14
+ typ == 'navigator.id.getAssertion'
15
+ end
16
+
17
+ def self.load_from_json(json)
18
+ client_data = ::JSON.parse(json)
19
+ instance = new
20
+ instance.typ = client_data['typ']
21
+ instance.challenge = client_data['challenge']
22
+ instance.origin = client_data['origin']
23
+ instance
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ module U2F
2
+ class Collection < Array
3
+ def initialize(array)
4
+ super([*array])
5
+ end
6
+
7
+ def to_json
8
+ "[#{map { |a| a.to_json }.join(',')}]"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module U2F
2
+ class Error < StandardError;end
3
+ class UnmatchedChallengeError < Error; end
4
+ class ClientDataTypeError < Error; end
5
+ class PublicKeyDecodeError < Error; end
6
+ class AttestationDecodeError < Error; end
7
+ class AttestationVerificationError < Error; end
8
+ class AttestationSignatureError < Error; end
9
+ class NoMatchingRequestError < Error; end
10
+ class NoMatchingRegistrationError < Error; end
11
+ class CounterToLowError < Error; end
12
+ class AuthenticationFailedError < Error; end
13
+ class UserNotPresentError < Error;end
14
+ end
@@ -0,0 +1,10 @@
1
+ module U2F
2
+ class RegisterRequest
3
+ include RequestBase
4
+
5
+ def initialize(challenge, app_id)
6
+ @challenge = challenge
7
+ @app_id = app_id
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,134 @@
1
+ module U2F
2
+ ##
3
+ # Representation of a U2F registration response.
4
+ # See chapter 4.3:
5
+ # http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf
6
+ class RegisterResponse
7
+ attr_accessor :client_data, :client_data_json, :registration_data_raw
8
+
9
+ PUBLIC_KEY_OFFSET = 1
10
+ PUBLIC_KEY_LENGTH = 65
11
+ KEY_HANDLE_LENGTH_LENGTH = 1
12
+ KEY_HANDLE_LENGTH_OFFSET = PUBLIC_KEY_OFFSET + PUBLIC_KEY_LENGTH
13
+ KEY_HANDLE_OFFSET = KEY_HANDLE_LENGTH_OFFSET + KEY_HANDLE_LENGTH_LENGTH
14
+
15
+ def self.load_from_json(json)
16
+ # TODO: validate
17
+ data = JSON.parse(json)
18
+ instance = new
19
+ instance.client_data_json =
20
+ Base64.urlsafe_decode64(data['clientData'])
21
+ instance.client_data =
22
+ ClientData.load_from_json(instance.client_data_json)
23
+ instance.registration_data_raw =
24
+ Base64.urlsafe_decode64(data['registrationData'])
25
+ instance
26
+ end
27
+
28
+ ##
29
+ # The attestation certificate in Base64 encoded X.509 DER format
30
+ def certificate
31
+ Base64.strict_encode64(certificate_raw)
32
+ end
33
+
34
+ ##
35
+ # Length of the attestation certificate
36
+ def certificate_length
37
+ # http://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One#Example_encoded_in_DER
38
+ #
39
+ # Do some quick parsing of the certificate DER format.
40
+ # First bytes (TLV) could be ex:
41
+ # T 0x30: SEQUENCE Tag
42
+ # L 0x82: Length (2 length bytes)
43
+ # 0x02 0xe2: Two bytes indicated by the L byte.
44
+ # Makes up the data length 738 (which makes 742 in total)
45
+
46
+ t_byte = certificate_bytes(0)
47
+
48
+ fail AttestationDecodeError unless t_byte == "\x30"
49
+
50
+ l_byte = certificate_bytes(1).unpack('c').first # 8-bit signed integer
51
+ # If the L-byte has MSB set to 1 (ie. < 0) the value will tell how many
52
+ # following bytes is used to describe the total length. Otherwise it will
53
+ # describe the data length
54
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/bb648641(v=vs.85).aspx
55
+
56
+ nbr_length_bytes = 0
57
+ cert_length = if l_byte < 0
58
+ nbr_length_bytes = l_byte + 0x80 # last 7-bits is the number of bytes
59
+ length_bytes = certificate_bytes(2, nbr_length_bytes).unpack('C*')
60
+ length_bytes.reverse.each_with_index.inject(0) do |sum, (val, idx)|
61
+ sum + (val << (8*idx))
62
+ end
63
+ else
64
+ l_byte
65
+ end
66
+
67
+ cert_length + nbr_length_bytes + 2 # Make up for the T and L bytes them selves
68
+ end
69
+
70
+ ##
71
+ # The attestation certificate in X.509 DER format
72
+ def certificate_raw
73
+ certificate_bytes(0, certificate_length)
74
+ end
75
+
76
+ ##
77
+ # Returns the key handle from registration data, URL safe base64 encoded
78
+ def key_handle
79
+ Base64.urlsafe_encode64(key_handle_raw)
80
+ end
81
+
82
+ def key_handle_raw
83
+ registration_data_raw.byteslice(KEY_HANDLE_OFFSET, key_handle_length)
84
+ end
85
+
86
+ ##
87
+ # Returns the length of the key handle, extracted from the registration data
88
+ def key_handle_length
89
+ registration_data_raw.byteslice(KEY_HANDLE_LENGTH_OFFSET).unpack('C').first
90
+ end
91
+
92
+ ##
93
+ # Returns the public key, extracted from the registration data
94
+ def public_key
95
+ # Base64 encode without linefeeds
96
+ Base64.strict_encode64(public_key_raw)
97
+ end
98
+
99
+ def public_key_raw
100
+ registration_data_raw.byteslice(PUBLIC_KEY_OFFSET, PUBLIC_KEY_LENGTH)
101
+ end
102
+
103
+ ##
104
+ # Returns the signature, extracted from the registration data
105
+ def signature
106
+ registration_data_raw.byteslice(
107
+ (KEY_HANDLE_OFFSET + key_handle_length + certificate_length)..-1)
108
+ end
109
+
110
+ ##
111
+ # Verifies the registration data agains the app id
112
+ def verify(app_id)
113
+ # Chapter 4.3 in
114
+ # http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf
115
+ data = [
116
+ "\x00",
117
+ Digest::SHA256.digest(app_id),
118
+ Digest::SHA256.digest(client_data_json),
119
+ key_handle_raw,
120
+ public_key_raw
121
+ ].join
122
+
123
+ cert = OpenSSL::X509::Certificate.new(certificate_raw)
124
+ cert.public_key.verify(OpenSSL::Digest::SHA256.new, signature, data)
125
+ end
126
+
127
+ private
128
+
129
+ def certificate_bytes(offset, length = 1)
130
+ base_offset = KEY_HANDLE_OFFSET + key_handle_length
131
+ registration_data_raw.byteslice(base_offset + offset, length)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,16 @@
1
+ module U2F
2
+ ##
3
+ # A representation of a registered U2F device
4
+ class Registration
5
+ attr_accessor :key_handle, :public_key, :certificate, :counter
6
+ def initialize(key_handle, public_key, certificate)
7
+ @key_handle = key_handle
8
+ @public_key = public_key
9
+ @certificate = certificate
10
+ end
11
+
12
+ def counter
13
+ @counter.nil? ? 0 : @counter
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ module U2F
2
+ module RequestBase
3
+ attr_accessor :version, :challenge, :app_id
4
+
5
+ def as_json
6
+ {
7
+ version: version,
8
+ challenge: challenge,
9
+ appId: app_id
10
+ }
11
+ end
12
+
13
+ def to_json
14
+ ::JSON.dump(as_json)
15
+ end
16
+
17
+ def version
18
+ 'U2F_V2'
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ module U2F
2
+ class SignRequest
3
+ include RequestBase
4
+ attr_accessor :key_handle
5
+
6
+ def initialize(key_handle, challenge, app_id)
7
+ @key_handle = key_handle
8
+ @challenge = challenge
9
+ @app_id = app_id
10
+ end
11
+
12
+ def as_json
13
+ super.merge(keyHandle: key_handle)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ module U2F
2
+ class SignResponse
3
+ attr_accessor :client_data, :client_data_json, :key_handle, :signature_data
4
+
5
+ def self.load_from_json(json)
6
+ data = ::JSON.parse(json)
7
+ instance = new
8
+ instance.client_data_json =
9
+ Base64.urlsafe_decode64(data['clientData'])
10
+ instance.client_data =
11
+ ClientData.load_from_json(instance.client_data_json)
12
+ instance.key_handle = data['keyHandle']
13
+ instance.signature_data =
14
+ Base64.urlsafe_decode64(data['signatureData'])
15
+ instance
16
+ end
17
+
18
+ ##
19
+ # Counter value that the U2F token increments every time it performs an
20
+ # authentication operation
21
+ def counter
22
+ signature_data[1..4].unpack('N').first
23
+ end
24
+
25
+ ##
26
+ # signature is to be verified using the public key obtained during
27
+ # registration.
28
+ def signature
29
+ signature_data.byteslice(5..-1)
30
+ end
31
+
32
+ ##
33
+ # If user presence was verified
34
+ def user_present?
35
+ signature_data[0].unpack('C').first == 1
36
+ end
37
+
38
+ ##
39
+ # Verifies the response against an app id and the public key of the
40
+ # registered device
41
+ def verify(app_id, public_key_pem)
42
+ data = [
43
+ Digest::SHA256.digest(app_id),
44
+ signature_data.byteslice(0, 5),
45
+ Digest::SHA256.digest(client_data_json)
46
+ ].join
47
+
48
+ public_key = OpenSSL::PKey.read(public_key_pem)
49
+ public_key.verify(OpenSSL::Digest::SHA256.new, signature, data)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,113 @@
1
+ module U2F
2
+ class U2F
3
+ attr_accessor :app_id
4
+ def initialize(app_id)
5
+ @app_id = app_id
6
+ end
7
+
8
+ ##
9
+ # Generate data to be sent to the U2F device before authenticating
10
+ def authentication_requests(key_handles)
11
+ key_handles = [key_handles] unless key_handles.is_a? Array
12
+ sign_requests = key_handles.map do |key_handle|
13
+ SignRequest.new(key_handle, challenge, app_id)
14
+ end
15
+ Collection.new(sign_requests)
16
+ end
17
+
18
+ ##
19
+ # Authenticate a response from the U2F device
20
+ def authenticate!(challenges, response, registration_public_key,
21
+ registration_counter)
22
+ # Handle both single and Array input
23
+ challenges = [challenges] unless challenges.is_a? Array
24
+
25
+ # TODO: check that it's the correct key_handle as well
26
+ unless challenges.include?(response.client_data.challenge)
27
+ fail NoMatchingRequestError
28
+ end
29
+
30
+ fail ClientDataTypeError unless response.client_data.authentication?
31
+
32
+ pem = U2F.public_key_pem(registration_public_key)
33
+
34
+ fail AuthenticationFailedError unless response.verify(app_id, pem)
35
+
36
+ fail UserNotPresentError unless response.user_present?
37
+
38
+ unless response.counter > registration_counter
39
+ fail CounterToLowError
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Generates a 32 byte long random U2F challenge
45
+ def challenge
46
+ Base64.urlsafe_encode64(SecureRandom.random_bytes(32))
47
+ end
48
+
49
+ ##
50
+ # Generate data to be used when registering a U2F device
51
+ def registration_requests
52
+ # TODO: generate a request for each supported version
53
+ Collection.new(RegisterRequest.new(challenge, @app_id))
54
+ end
55
+
56
+ ##
57
+ # Authenticate the response from the U2F device when registering
58
+ # Returns a registration object
59
+ def register!(challenges, response)
60
+ challenges = [challenges] unless challenges.is_a? Array
61
+ challenge = challenges.detect do |chg|
62
+ chg == response.client_data.challenge
63
+ end
64
+
65
+ fail UnmatchedChallengeError unless challenge
66
+
67
+ fail ClientDataTypeError unless response.client_data.registration?
68
+
69
+ # Validate public key
70
+ U2F.public_key_pem(response.public_key_raw)
71
+
72
+ unless U2F.validate_certificate(response.certificate_raw)
73
+ fail AttestationVerificationError
74
+ end
75
+
76
+ fail AttestationSignatureError unless response.verify(app_id)
77
+
78
+ registration = Registration.new(
79
+ response.key_handle,
80
+ response.public_key,
81
+ response.certificate
82
+ )
83
+ registration
84
+ end
85
+
86
+ ##
87
+ # Convert a binary public key to PEM format
88
+ def self.public_key_pem(key)
89
+ fail PublicKeyDecodeError unless key.length == 65 || key[0] == "\x04"
90
+ # http://tools.ietf.org/html/rfc5480
91
+ der = OpenSSL::ASN1::Sequence([
92
+ OpenSSL::ASN1::Sequence([
93
+ OpenSSL::ASN1::ObjectId('1.2.840.10045.2.1'), # id-ecPublicKey
94
+ OpenSSL::ASN1::ObjectId('1.2.840.10045.3.1.7') # secp256r1
95
+ ]),
96
+ OpenSSL::ASN1::BitString(key)
97
+ ]).to_der
98
+
99
+ pem = "-----BEGIN PUBLIC KEY-----\r\n" +
100
+ Base64.strict_encode64(der).scan(/.{1,64}/).join("\r\n") +
101
+ "\r\n-----END PUBLIC KEY-----"
102
+ pem
103
+ end
104
+
105
+ def self.validate_certificate(certificate_raw)
106
+ # TODO
107
+ return true
108
+ # cacert = OpenSSL::X509::Certificate.new()
109
+ # cert = OpenSSL::X509::Certificate.new(certificate_raw)
110
+ # cert.verify(cacert.public_key)
111
+ end
112
+ end
113
+ end
@@ -1,3 +1,3 @@
1
1
  module U2F
2
- VERSION = "0.0.0"
2
+ VERSION = "0.0.1"
3
3
  end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe U2F::ClientData do
4
+ let(:type) { '' }
5
+ let(:registration_type) { 'navigator.id.finishEnrollment' }
6
+ let(:authentication_type) { 'navigator.id.getAssertion' }
7
+
8
+ let(:client_data) do
9
+ cd = U2F::ClientData.new
10
+ cd.typ = type
11
+ cd
12
+ end
13
+
14
+ describe '#registration?' do
15
+ subject { client_data.registration? }
16
+ context 'for correct type' do
17
+ let(:type) { registration_type }
18
+ it { is_expected.to be_truthy }
19
+ end
20
+
21
+ context 'for incorrect type' do
22
+ let(:type) { authentication_type }
23
+ it { is_expected.to be_falsey }
24
+ end
25
+ end
26
+
27
+ describe '#authentication?' do
28
+ subject { client_data.authentication? }
29
+ context 'for correct type' do
30
+ let(:type) { authentication_type }
31
+ it { is_expected.to be_truthy }
32
+ end
33
+
34
+ context 'for incorrect type' do
35
+ let(:type) { registration_type }
36
+ it { is_expected.to be_falsey }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe U2F::Collection do
4
+ let(:collection) { U2F::Collection.new(input) }
5
+ describe '#to_json' do
6
+ subject { collection.to_json }
7
+ context 'with single object' do
8
+ let(:input) { 'one' }
9
+ it do
10
+ is_expected.to match_json_expression(['one'])
11
+ end
12
+ end
13
+ context 'with single object' do
14
+ let(:input) { ['one', 'two'] }
15
+ it do
16
+ is_expected.to match_json_expression(['one', 'two'])
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe U2F::RegisterRequest do
4
+ let(:app_id) { 'http://example.com' }
5
+ let(:challenge) { 'fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g' }
6
+
7
+ let(:sign_request) do
8
+ U2F::RegisterRequest.new(challenge, app_id)
9
+ end
10
+
11
+ describe '#to_json' do
12
+ subject { sign_request.to_json }
13
+ it do
14
+ is_expected.to match_json_expression(
15
+ version: String,
16
+ appId: String,
17
+ challenge: String
18
+ )
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe U2F::RegisterResponse do
4
+ let(:key_handle) do
5
+ 'CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w=='
6
+ end
7
+ let(:public_key) do
8
+ 'BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y/yaFORPUe3c='
9
+ end
10
+ let(:certificate) do
11
+ 'MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp/VRZHOwd2NZNzpnB9ePNKvUaWCGK/gN+cynnYFdwJ75iSgMVYb/RnFcdPwnsBzBU68hbhTnu/FvJxWo7rZJ2q7qXpA10eLVXJr4/4oSXEk9I/0IIHqOP98Ck/fAoI5gYI7ygndyqoPJ/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh/h7oKEKamCWk19dJp5jHQmumkHlvQhH/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg/0J+xOb4zl6a1z65nae4OTj7628/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=='
12
+ end
13
+ let(:registration_data_json) do
14
+ '{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'
15
+ end
16
+
17
+ let(:app_id) { 'http://demo.example.com' }
18
+ let(:challenge) { 'yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8' }
19
+
20
+ let(:registration_request) { U2F::RegisterRequest.new(challenge, app_id) }
21
+
22
+ let(:register_response) do
23
+ U2F::RegisterResponse.load_from_json(registration_data_json)
24
+ end
25
+
26
+ describe '#certificate' do
27
+ subject { register_response.certificate }
28
+ it { is_expected.to eq certificate }
29
+ end
30
+
31
+ describe '#client_data' do
32
+ context 'challenge' do
33
+ subject { register_response.client_data.challenge }
34
+ it { is_expected.to eq challenge }
35
+ end
36
+ end
37
+
38
+ describe '#key_handle' do
39
+ subject { register_response.key_handle }
40
+ it { is_expected.to eq key_handle }
41
+ end
42
+
43
+ describe '#key_handle_length' do
44
+ subject { register_response.key_handle_length }
45
+ it { is_expected.to eq Base64.urlsafe_decode64(key_handle).length }
46
+ end
47
+
48
+ describe '#public_key' do
49
+ subject { register_response.public_key }
50
+ it { is_expected.to eq public_key }
51
+ end
52
+
53
+ describe '#verify' do
54
+ subject { register_response.verify(app_id) }
55
+ it { is_expected.to be_truthy }
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe U2F::SignRequest do
4
+ let(:app_id) { 'http://example.com' }
5
+ let(:challenge) { 'fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g' }
6
+ let(:key_handle) do
7
+ 'CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w=='
8
+ end
9
+ let(:sign_request) do
10
+ U2F::SignRequest.new(key_handle, challenge, app_id)
11
+ end
12
+
13
+ describe '#to_json' do
14
+ subject { sign_request.to_json }
15
+ it do
16
+ is_expected.to match_json_expression(
17
+ version: String,
18
+ appId: String,
19
+ challenge: String,
20
+ keyHandle: String
21
+ )
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe U2F::SignResponse do
4
+ let(:json_response) do
5
+ '{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w==" }'
6
+ end
7
+ let(:sign_response) do
8
+ U2F::SignResponse.load_from_json json_response
9
+ end
10
+
11
+ describe '#counter' do
12
+ subject { sign_response.counter }
13
+ it { is_expected.to be 4 }
14
+ end
15
+
16
+ describe '#user_present?' do
17
+ subject { sign_response.user_present? }
18
+ it { is_expected.to be true }
19
+ end
20
+ end
@@ -0,0 +1,129 @@
1
+ require 'spec_helper'
2
+
3
+ describe U2F do
4
+ let(:app_id) { 'http://demo.example.com' }
5
+ let(:u2f) { U2F::U2F.new(app_id) }
6
+ let(:challenge) { 'fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g' }
7
+ let(:key_handle) do
8
+ 'CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w=='
9
+ end
10
+ let(:certificate) do
11
+ "MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="
12
+ end
13
+ let(:registration_data_json) do
14
+ '{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'
15
+ end
16
+ let(:public_key) do
17
+ Base64.urlsafe_decode64("BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=")
18
+ end
19
+ let(:json_response) do
20
+ '{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w==" }'
21
+ end
22
+ let(:registration) do
23
+ U2F::Registration.new(key_handle, public_key, certificate)
24
+ end
25
+ let(:register_response) do
26
+ U2F::RegisterResponse.load_from_json(registration_data_json)
27
+ end
28
+ let(:response) do
29
+ U2F::SignResponse.load_from_json json_response
30
+ end
31
+ let(:sign_request) do
32
+ U2F::SignRequest.new(key_handle, challenge, app_id)
33
+ end
34
+
35
+ describe '#authentication_requests' do
36
+ let(:requests) { u2f.authentication_requests(key_handle) }
37
+ it 'returns an array of requests' do
38
+ expect(requests).to be_an Array
39
+ requests.each { |r| expect(r).to be_a U2F::SignRequest }
40
+ end
41
+ end
42
+
43
+ describe '#authenticate!' do
44
+ let(:counter) { registration.counter }
45
+ let(:reg_public_key) { registration.public_key }
46
+ let (:u2f_authenticate) do
47
+ u2f.authenticate!(challenge, response, reg_public_key, counter)
48
+ end
49
+ context 'with correct parameters' do
50
+ it 'does not raise an error' do
51
+ expect { u2f_authenticate }.to_not raise_error
52
+ end
53
+ end
54
+
55
+ context 'with incorrect challenge' do
56
+ let(:challenge) { 'incorrect' }
57
+ it 'raises NoMatchingRequestError' do
58
+ expect { u2f_authenticate }.to raise_error(U2F::NoMatchingRequestError)
59
+ end
60
+ end
61
+
62
+ context 'with incorrect counter' do
63
+ let(:counter) { 1000 }
64
+ it 'raises CounterToLowError' do
65
+ expect { u2f_authenticate }.to raise_error(U2F::CounterToLowError)
66
+ end
67
+ end
68
+ context 'with incorrect counter' do
69
+ let(:reg_public_key) { "\x00" }
70
+ it 'raises CounterToLowError' do
71
+ expect { u2f_authenticate }.to raise_error(U2F::PublicKeyDecodeError)
72
+ end
73
+ end
74
+ end
75
+
76
+ describe '#registration_requests' do
77
+ let(:requests) { u2f.registration_requests }
78
+ it 'returns an array of requests' do
79
+ expect(requests).to be_an Array
80
+ requests.each { |r| expect(r).to be_a U2F::RegisterRequest }
81
+ end
82
+ end
83
+
84
+ describe '#register!' do
85
+ let(:challenge) { 'yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8' }
86
+ context 'with correct registration data' do
87
+ it 'returns a registration' do
88
+ reg = nil
89
+ expect {
90
+ reg = u2f.register!(challenge, register_response)
91
+ }.to_not 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', challenge], register_response)
97
+ expect(reg).to be_a U2F::Registration
98
+ end
99
+ end
100
+
101
+ context 'with unknown challenge' do
102
+ let(:challenge) { 'non-matching' }
103
+ it 'raises an UnmatchedChallengeError' do
104
+ expect {
105
+ u2f.register!(challenge, register_response)
106
+ }.to raise_error(U2F::UnmatchedChallengeError)
107
+ end
108
+ end
109
+ end
110
+
111
+ describe '::public_key_pem' do
112
+ context 'with correct key' do
113
+ it 'wraps the result' do
114
+ pem = U2F::U2F.public_key_pem public_key
115
+ expect(pem).to start_with '-----BEGIN PUBLIC KEY-----'
116
+ expect(pem).to end_with '-----END PUBLIC KEY-----'
117
+ end
118
+ end
119
+
120
+ context 'with incorrect key' do
121
+ let(:public_key) { Base64.urlsafe_decode64('NW5jdzdnODV3dm9nNzU4d2duNTd3') }
122
+ it 'fails when key is to short' do
123
+ expect {
124
+ U2F::U2F.public_key_pem public_key
125
+ }.to raise_error(U2F::PublicKeyDecodeError)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,5 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ require 'json_expressions/rspec'
5
+ require 'u2f'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: u2f
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johan Brissmyr
@@ -9,8 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-10-31 00:00:00.000000000 Z
12
+ date: 2014-11-05 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: rspec
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -25,6 +39,48 @@ dependencies:
25
39
  - - ">="
26
40
  - !ruby/object:Gem::Version
27
41
  version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: json_expressions
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rubocop
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: coveralls
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
28
84
  description: U2F library
29
85
  email:
30
86
  - brissmyr@gmail.com
@@ -35,7 +91,25 @@ extra_rdoc_files: []
35
91
  files:
36
92
  - README.md
37
93
  - lib/u2f.rb
94
+ - lib/u2f/client_data.rb
95
+ - lib/u2f/collection.rb
96
+ - lib/u2f/errors.rb
97
+ - lib/u2f/register_request.rb
98
+ - lib/u2f/register_response.rb
99
+ - lib/u2f/registration.rb
100
+ - lib/u2f/request_base.rb
101
+ - lib/u2f/sign_request.rb
102
+ - lib/u2f/sign_response.rb
103
+ - lib/u2f/u2f.rb
38
104
  - lib/version.rb
105
+ - spec/lib/client_data_spec.rb
106
+ - spec/lib/collection_spec.rb
107
+ - spec/lib/register_request_spec.rb
108
+ - spec/lib/register_response_spec.rb
109
+ - spec/lib/sign_request_spec.rb
110
+ - spec/lib/sign_response_spec.rb
111
+ - spec/lib/u2f_spec.rb
112
+ - spec/spec_helper.rb
39
113
  homepage: https://github.com/userbin/ruby-u2f
40
114
  licenses:
41
115
  - MIT
@@ -60,4 +134,12 @@ rubygems_version: 2.2.2
60
134
  signing_key:
61
135
  specification_version: 4
62
136
  summary: U2F library
63
- test_files: []
137
+ test_files:
138
+ - spec/lib/client_data_spec.rb
139
+ - spec/lib/collection_spec.rb
140
+ - spec/lib/register_request_spec.rb
141
+ - spec/lib/register_response_spec.rb
142
+ - spec/lib/sign_request_spec.rb
143
+ - spec/lib/sign_response_spec.rb
144
+ - spec/lib/u2f_spec.rb
145
+ - spec/spec_helper.rb