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 +4 -4
- data/README.md +204 -1
- data/lib/u2f.rb +15 -0
- data/lib/u2f/client_data.rb +26 -0
- data/lib/u2f/collection.rb +11 -0
- data/lib/u2f/errors.rb +14 -0
- data/lib/u2f/register_request.rb +10 -0
- data/lib/u2f/register_response.rb +134 -0
- data/lib/u2f/registration.rb +16 -0
- data/lib/u2f/request_base.rb +21 -0
- data/lib/u2f/sign_request.rb +16 -0
- data/lib/u2f/sign_response.rb +52 -0
- data/lib/u2f/u2f.rb +113 -0
- data/lib/version.rb +1 -1
- data/spec/lib/client_data_spec.rb +39 -0
- data/spec/lib/collection_spec.rb +20 -0
- data/spec/lib/register_request_spec.rb +21 -0
- data/spec/lib/register_response_spec.rb +57 -0
- data/spec/lib/sign_request_spec.rb +24 -0
- data/spec/lib/sign_response_spec.rb +20 -0
- data/spec/lib/u2f_spec.rb +129 -0
- data/spec/spec_helper.rb +5 -0
- metadata +85 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 047cac5bb30fd3ec7ab0d032ae465e6d9e9cadc0
|
4
|
+
data.tar.gz: 041fe8a755ec15ddd949523f61ed0863d646aae5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 829483f4451930d2eb42bab1f95b68b8c96b128b9068079592074c4f4e3365bc920c9f5e0539a534091ce253a19f375ef7521de2e35ae206f57d2b0dded41fa5
|
7
|
+
data.tar.gz: 43719723ce28854b37528e208cf5bdfe8d2ed69479e088af45c41388b46c3ab4a2c34b17ba3b5b1c6812e77b5d355360216d3929e54320562604c59dea0fc735
|
data/README.md
CHANGED
@@ -1,3 +1,206 @@
|
|
1
1
|
# Ruby U2F
|
2
2
|
|
3
|
-
|
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
|
data/lib/u2f/errors.rb
ADDED
@@ -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,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
|
data/lib/u2f/u2f.rb
ADDED
@@ -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
|
data/lib/version.rb
CHANGED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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.
|
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-
|
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
|