u2f 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
[](http://badge.fury.io/rb/u2f)
|
4
|
+
[](https://travis-ci.org/userbin/ruby-u2f)
|
5
|
+
[](https://codeclimate.com/github/userbin/ruby-u2f)
|
6
|
+
[](https://coveralls.io/r/userbin/ruby-u2f)
|
7
|
+
[](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
|