u2f 0.1.0 → 0.2.0
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 +5 -3
- data/lib/u2f.rb +2 -0
- data/lib/u2f/client_data.rb +5 -2
- data/lib/u2f/fake_u2f.rb +194 -0
- data/lib/u2f/register_response.rb +3 -3
- data/lib/u2f/request_base.rb +1 -1
- data/lib/u2f/sign_request.rb +1 -1
- data/lib/u2f/sign_response.rb +3 -3
- data/lib/u2f/u2f.rb +1 -1
- data/lib/version.rb +1 -1
- data/spec/lib/client_data_spec.rb +2 -2
- data/spec/lib/register_response_spec.rb +11 -25
- data/spec/lib/sign_response_spec.rb +6 -7
- data/spec/lib/u2f_spec.rb +30 -30
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4181871c3a8f8591e810fca9f378c917d3c1817f
|
4
|
+
data.tar.gz: 537078ef16ddaf5392b56e5e7c072f6d908a8e71
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f7a5df9ff90a60b12d979e3e4b23f708dc846b8962083fcc12528c362223f7d4512b972802b79b065d90f1a6b3d120ed7a2c1a7cb4878e43a75d3b6bd017b2e
|
7
|
+
data.tar.gz: 9bceaa3d4c0ed8529a69d903731fbbcd0fe616d4d976af5c321c56b8b73337437e6c017bc82dfde83f2c4bcb55035ef44be42918f6028365da808b786e97843a
|
data/README.md
CHANGED
@@ -19,6 +19,8 @@ U2F is an open 2-factor authentication standard that enables keychain devices, m
|
|
19
19
|
|
20
20
|
Check out the [example](https://github.com/castle/ruby-u2f/tree/master/example) directory for a fully working Padrino server demonstrating U2F.
|
21
21
|
|
22
|
+
There is another demo application available using the [Cuba](https://github.com/soveran/cuba) framework: [cuba-u2f-demo](https://github.com/badboy/cuba-u2f-demo) and a [blog post explaining the protocol and the implementation](http://fnordig.de/2015/03/06/u2f-demo-application/).
|
23
|
+
|
22
24
|
## Installation
|
23
25
|
|
24
26
|
Add the `u2f` gem to your `Gemfile`
|
@@ -84,8 +86,8 @@ Render a form that will be automatically posted when the U2F device reponds.
|
|
84
86
|
|
85
87
|
```javascript
|
86
88
|
// render requests from server into Javascript format
|
87
|
-
var registerRequests = <%= @registration_requests.to_json.html_safe %>;
|
88
|
-
var signRequests = <%= @sign_requests.to_json.html_safe %>;
|
89
|
+
var registerRequests = <%= @registration_requests.as_json.to_json.html_safe %>;
|
90
|
+
var signRequests = <%= @sign_requests.as_json.to_json.html_safe %>;
|
89
91
|
|
90
92
|
u2f.register(registerRequests, signRequests, function(registerResponse) {
|
91
93
|
var form, reg;
|
@@ -160,7 +162,7 @@ Render a form that will be automatically posted when the U2F device reponds.
|
|
160
162
|
|
161
163
|
```javascript
|
162
164
|
// render requests from server into Javascript format
|
163
|
-
var signRequests = <%= @sign_requests.to_json.html_safe %>;
|
165
|
+
var signRequests = <%= @sign_requests.as_json.to_json.html_safe %>;
|
164
166
|
|
165
167
|
u2f.sign(signRequests, function(signResponse) {
|
166
168
|
var form, reg;
|
data/lib/u2f.rb
CHANGED
data/lib/u2f/client_data.rb
CHANGED
@@ -3,15 +3,18 @@ module U2F
|
|
3
3
|
# A representation of ClientData, chapter 7
|
4
4
|
# http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf
|
5
5
|
class ClientData
|
6
|
+
REGISTRATION_TYP = "navigator.id.finishEnrollment".freeze
|
7
|
+
AUTHENTICATION_TYP = "navigator.id.getAssertion".freeze
|
8
|
+
|
6
9
|
attr_accessor :typ, :challenge, :origin
|
7
10
|
alias_method :type, :typ
|
8
11
|
|
9
12
|
def registration?
|
10
|
-
typ ==
|
13
|
+
typ == REGISTRATION_TYP
|
11
14
|
end
|
12
15
|
|
13
16
|
def authentication?
|
14
|
-
typ ==
|
17
|
+
typ == AUTHENTICATION_TYP
|
15
18
|
end
|
16
19
|
|
17
20
|
def self.load_from_json(json)
|
data/lib/u2f/fake_u2f.rb
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
class U2F::FakeU2F
|
2
|
+
CURVE_NAME = "prime256v1".freeze
|
3
|
+
|
4
|
+
attr_accessor :app_id, :counter, :key_handle_raw, :cert_subject
|
5
|
+
|
6
|
+
# Initialize a new FakeU2F device for use in tests.
|
7
|
+
#
|
8
|
+
# app_id - The appId/origin this is being tested against.
|
9
|
+
# options - A Hash of optional parameters (optional).
|
10
|
+
# :counter - The initial counter for this device.
|
11
|
+
# :key_handle - The raw key-handle this device should use.
|
12
|
+
# :cert_subject - The subject field for the certificate generated
|
13
|
+
# for this device.
|
14
|
+
#
|
15
|
+
# Returns nothing.
|
16
|
+
def initialize(app_id, options = {})
|
17
|
+
@app_id = app_id
|
18
|
+
@counter = options.fetch(:counter, 0)
|
19
|
+
@key_handle_raw = options.fetch(:key_handle, SecureRandom.random_bytes(32))
|
20
|
+
@cert_subject = options.fetch(:cert_subject, "/CN=U2FTest")
|
21
|
+
end
|
22
|
+
|
23
|
+
# A registerResponse hash as returned by the u2f.register JavaScript API.
|
24
|
+
#
|
25
|
+
# challenge - The challenge to sign.
|
26
|
+
# error - Boolean. Whether to return an error response (optional).
|
27
|
+
#
|
28
|
+
# Returns a JSON encoded Hash String.
|
29
|
+
def register_response(challenge, error = false)
|
30
|
+
if error
|
31
|
+
JSON.dump(:errorCode => 4)
|
32
|
+
else
|
33
|
+
client_data_json = client_data(U2F::ClientData::REGISTRATION_TYP, challenge)
|
34
|
+
JSON.dump(
|
35
|
+
:registrationData => reg_registration_data(client_data_json),
|
36
|
+
:clientData => U2F.urlsafe_encode64(client_data_json)
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# A SignResponse hash as returned by the u2f.sign JavaScript API.
|
42
|
+
#
|
43
|
+
# challenge - The challenge to sign.
|
44
|
+
#
|
45
|
+
# Returns a JSON encoded Hash String.
|
46
|
+
def sign_response(challenge)
|
47
|
+
client_data_json = client_data(U2F::ClientData::AUTHENTICATION_TYP, challenge)
|
48
|
+
JSON.dump(
|
49
|
+
:clientData => U2F.urlsafe_encode64(client_data_json),
|
50
|
+
:keyHandle => U2F.urlsafe_encode64(key_handle_raw),
|
51
|
+
:signatureData => auth_signature_data(client_data_json)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# The appId specific public key as returned in the registrationData field of
|
56
|
+
# a RegisterResponse Hash.
|
57
|
+
#
|
58
|
+
# Returns a binary formatted EC public key String.
|
59
|
+
def origin_public_key_raw
|
60
|
+
[origin_key.public_key.to_bn.to_s(16)].pack('H*')
|
61
|
+
end
|
62
|
+
|
63
|
+
# The raw device attestation certificate as returned in the registrationData
|
64
|
+
# field of a RegisterResponse Hash.
|
65
|
+
#
|
66
|
+
# Returns a DER formatted certificate String.
|
67
|
+
def cert_raw
|
68
|
+
cert.to_der
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# The registrationData field returns in a RegisterResponse Hash.
|
74
|
+
#
|
75
|
+
# client_data_json - The JSON encoded clientData String.
|
76
|
+
#
|
77
|
+
# Returns a url-safe base64 encoded binary String.
|
78
|
+
def reg_registration_data(client_data_json)
|
79
|
+
U2F.urlsafe_encode64(
|
80
|
+
[
|
81
|
+
5,
|
82
|
+
origin_public_key_raw,
|
83
|
+
key_handle_raw.bytesize,
|
84
|
+
key_handle_raw,
|
85
|
+
cert_raw,
|
86
|
+
reg_signature(client_data_json)
|
87
|
+
].pack("CA65CA#{key_handle_raw.bytesize}A#{cert_raw.bytesize}A*")
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
# The signature field of a registrationData field of a RegisterResponse.
|
92
|
+
#
|
93
|
+
# client_data_json - The JSON encoded clientData String.
|
94
|
+
#
|
95
|
+
# Returns an ECDSA signature String.
|
96
|
+
def reg_signature(client_data_json)
|
97
|
+
payload = [
|
98
|
+
"\x00",
|
99
|
+
U2F::DIGEST.digest(app_id),
|
100
|
+
U2F::DIGEST.digest(client_data_json),
|
101
|
+
key_handle_raw,
|
102
|
+
origin_public_key_raw
|
103
|
+
].join
|
104
|
+
cert_key.sign(U2F::DIGEST.new, payload)
|
105
|
+
end
|
106
|
+
|
107
|
+
# The signatureData field of a SignResponse Hash.
|
108
|
+
#
|
109
|
+
# client_data_json - The JSON encoded clientData String.
|
110
|
+
#
|
111
|
+
# Returns a url-safe base64 encoded binary String.
|
112
|
+
def auth_signature_data(client_data_json)
|
113
|
+
::U2F.urlsafe_encode64(
|
114
|
+
[
|
115
|
+
1, # User present
|
116
|
+
self.counter += 1,
|
117
|
+
auth_signature(client_data_json)
|
118
|
+
].pack("CNA*")
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
# The signature field of a signatureData field of a SignResponse Hash.
|
123
|
+
#
|
124
|
+
# client_data_json - The JSON encoded clientData String.
|
125
|
+
#
|
126
|
+
# Returns an ECDSA signature String.
|
127
|
+
def auth_signature(client_data_json)
|
128
|
+
data = [
|
129
|
+
U2F::DIGEST.digest(app_id),
|
130
|
+
1, # User present
|
131
|
+
counter,
|
132
|
+
U2F::DIGEST.digest(client_data_json)
|
133
|
+
].pack("A32CNA32")
|
134
|
+
|
135
|
+
origin_key.sign(U2F::DIGEST.new, data)
|
136
|
+
end
|
137
|
+
|
138
|
+
# The clientData hash as returned by registration and authentication
|
139
|
+
# responses.
|
140
|
+
#
|
141
|
+
# typ - The String value for the 'typ' field.
|
142
|
+
# challenge - The String url-safe base64 encoded challenge parameter.
|
143
|
+
#
|
144
|
+
# Returns a JSON encoded Hash String.
|
145
|
+
def client_data(typ, challenge)
|
146
|
+
JSON.dump(
|
147
|
+
:challenge => challenge,
|
148
|
+
:origin => app_id,
|
149
|
+
:typ => typ
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
# The appId-specific public/private key.
|
154
|
+
#
|
155
|
+
# Returns a OpenSSL::PKey::EC instance.
|
156
|
+
def origin_key
|
157
|
+
@origin_key ||= generate_ec_key
|
158
|
+
end
|
159
|
+
|
160
|
+
# The self-signed device attestation certificate.
|
161
|
+
#
|
162
|
+
# Returns a OpenSSL::X509::Certificate instance.
|
163
|
+
def cert
|
164
|
+
@cert ||= OpenSSL::X509::Certificate.new.tap do |c|
|
165
|
+
c.subject = c.issuer = OpenSSL::X509::Name.parse(cert_subject)
|
166
|
+
c.not_before = Time.now
|
167
|
+
c.not_after = Time.now + 365 * 24 * 60 * 60
|
168
|
+
c.public_key = cert_key
|
169
|
+
c.serial = 0x1
|
170
|
+
c.version = 0x0
|
171
|
+
c.sign cert_key, U2F::DIGEST.new
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# The public key used for signing the device certificate.
|
176
|
+
#
|
177
|
+
# Returns a OpenSSL::PKey::EC instance.
|
178
|
+
def cert_key
|
179
|
+
@cert_key ||= generate_ec_key
|
180
|
+
end
|
181
|
+
|
182
|
+
# Generate an eliptic curve public/private key.
|
183
|
+
#
|
184
|
+
# Returns a OpenSSL::PKey::EC instance.
|
185
|
+
def generate_ec_key
|
186
|
+
OpenSSL::PKey::EC.new().tap do |ec|
|
187
|
+
ec.group = OpenSSL::PKey::EC::Group.new(CURVE_NAME)
|
188
|
+
ec.generate_key
|
189
|
+
# https://bugs.ruby-lang.org/issues/8177
|
190
|
+
ec.define_singleton_method(:private?) { private_key? }
|
191
|
+
ec.define_singleton_method(:public?) { public_key? }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -89,13 +89,13 @@ module U2F
|
|
89
89
|
# http://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf
|
90
90
|
data = [
|
91
91
|
"\x00",
|
92
|
-
|
93
|
-
|
92
|
+
::U2F::DIGEST.digest(app_id),
|
93
|
+
::U2F::DIGEST.digest(client_data_json),
|
94
94
|
key_handle_raw,
|
95
95
|
public_key_raw
|
96
96
|
].join
|
97
97
|
|
98
|
-
parsed_certificate.public_key.verify(
|
98
|
+
parsed_certificate.public_key.verify(::U2F::DIGEST.new, signature, data)
|
99
99
|
end
|
100
100
|
|
101
101
|
private
|
data/lib/u2f/request_base.rb
CHANGED
data/lib/u2f/sign_request.rb
CHANGED
data/lib/u2f/sign_response.rb
CHANGED
@@ -40,13 +40,13 @@ module U2F
|
|
40
40
|
# registered device
|
41
41
|
def verify(app_id, public_key_pem)
|
42
42
|
data = [
|
43
|
-
|
43
|
+
::U2F::DIGEST.digest(app_id),
|
44
44
|
signature_data.byteslice(0, 5),
|
45
|
-
|
45
|
+
::U2F::DIGEST.digest(client_data_json)
|
46
46
|
].join
|
47
47
|
|
48
48
|
public_key = OpenSSL::PKey.read(public_key_pem)
|
49
|
-
public_key.verify(
|
49
|
+
public_key.verify(::U2F::DIGEST.new, signature, data)
|
50
50
|
end
|
51
51
|
end
|
52
52
|
end
|
data/lib/u2f/u2f.rb
CHANGED
@@ -140,7 +140,7 @@ module U2F
|
|
140
140
|
# - +PublicKeyDecodeError+:: if the +key+ argument is incorrect
|
141
141
|
#
|
142
142
|
def self.public_key_pem(key)
|
143
|
-
fail PublicKeyDecodeError unless key.length == 65
|
143
|
+
fail PublicKeyDecodeError unless key.length == 65 && key[0] == "\x04"
|
144
144
|
# http://tools.ietf.org/html/rfc5480
|
145
145
|
der = OpenSSL::ASN1::Sequence([
|
146
146
|
OpenSSL::ASN1::Sequence([
|
data/lib/version.rb
CHANGED
@@ -2,8 +2,8 @@ require 'spec_helper.rb'
|
|
2
2
|
|
3
3
|
describe U2F::ClientData do
|
4
4
|
let(:type) { '' }
|
5
|
-
let(:registration_type) {
|
6
|
-
let(:authentication_type) {
|
5
|
+
let(:registration_type) { U2F::ClientData::REGISTRATION_TYP }
|
6
|
+
let(:authentication_type) { U2F::ClientData::AUTHENTICATION_TYP }
|
7
7
|
|
8
8
|
let(:client_data) do
|
9
9
|
cd = U2F::ClientData.new
|
@@ -1,32 +1,18 @@
|
|
1
1
|
require 'spec_helper.rb'
|
2
2
|
|
3
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(:registration_data_without_padding) {
|
18
|
-
"{\"registrationData\":\"BQT2UXxw7PXHmN5nCj1M3Lq_sibfqQehZbuUV1Vxr1l0J1Gdcv7FEvnPofmrSN44_pz8-XAj7pOpqB79rOphJPf2QM8nt8Jtyyj9_XmZWZTQMg2UVHvrin_Jc4tMHY9QmyCNDmSU9_Bhb-Ei4u5GPgLrpF1TaEYQCqUHboqDKt4x524wggIbMIIBBaADAgECAgR1o_Z1MAsGCSqGSIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTk3MzY3OTczMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBmjfkNqa2mXzVh2ZxuES5coCvvENxDMDLmfd-0ACG0Fu7wR4ZTjKd9KAuidySpfona5csGmlM0Te_Zu35h_wwujEjAQMA4GCisGAQQBgsQKAQIEADALBgkqhkiG9w0BAQsDggEBAb0tuI0-CzSxBg4cAlyD6UyT4cKyJZGVhWdtPgj_mWepT3Tu9jXtdgA5F3jfZtTc2eGxuS-PPvqRAkZd40AXgM8A0YaXPwlT4s0RUTY9Y8aAQzQZeAHuZk3lKKd_LUCg5077dzdt90lC5eVTEduj6cOnHEqnOr2Cv75FuiQXX7QkGQxtoD-otgvhZ2Fjk29o7Iy9ik7ewHGXOfoVw_ruGWi0YfXBTuqEJ6H666vvMN4BZWHtzhC0k5ceQslB9Xdntky-GQgDqNkkBf32GKwAFT9JJrkO2BfsB-wfBrTiHr0AABYNTNKTceA5dtR3UVpI492VUWQbY3YmWUUfKTI7fM4wRgIhAIfEKaF0w43L3RJHXp8qeRKw8Ek0CVcZ6pvBsH3Wo3F1AiEA5w89AFOBrjoSsnuGdUgB4AGxc5bRnV-p8jGUNoVSUwI\",\"version\":\"U2F_V2\",\"challenge\":\"oqDO4u_tTvhm1LhFDVYhFwywQF0PzFsXPgjD-5lKGDY\",\"appId\":\"http://localhost:3000\",\"clientData\":\"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCIsImNoYWxsZW5nZSI6Im9xRE80dV90VHZobTFMaEZEVlloRnd5d1FGMFB6RnNYUGdqRC01bEtHRFk9Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiY2lkX3B1YmtleSI6IiJ9\"}"
|
19
|
-
}
|
20
|
-
|
21
|
-
let(:error_response) {
|
22
|
-
"{\"errorCode\":4}"
|
23
|
-
}
|
24
|
-
|
25
4
|
let(:app_id) { 'http://demo.example.com' }
|
26
|
-
let(:challenge) {
|
27
|
-
|
5
|
+
let(:challenge) { U2F.urlsafe_encode64(SecureRandom.random_bytes(32)) }
|
6
|
+
let(:device) { U2F::FakeU2F.new(app_id) }
|
7
|
+
let(:key_handle) { U2F.urlsafe_encode64(device.key_handle_raw) }
|
8
|
+
let(:public_key) { Base64.strict_encode64(device.origin_public_key_raw) }
|
9
|
+
let(:certificate) { Base64.strict_encode64(device.cert_raw) }
|
10
|
+
let(:registration_data_json) { device.register_response(challenge) }
|
11
|
+
let(:registration_data_json_without_padding) do
|
12
|
+
device.register_response(challenge).gsub(" ", "")
|
13
|
+
end
|
14
|
+
let(:error_response) { device.register_response(challenge, error = true) }
|
28
15
|
let(:registration_request) { U2F::RegisterRequest.new(challenge, app_id) }
|
29
|
-
|
30
16
|
let(:register_response) do
|
31
17
|
U2F::RegisterResponse.load_from_json(registration_data_json)
|
32
18
|
end
|
@@ -43,7 +29,7 @@ describe U2F::RegisterResponse do
|
|
43
29
|
end
|
44
30
|
|
45
31
|
context 'with unpadded response' do
|
46
|
-
let(:registration_data_json) {
|
32
|
+
let(:registration_data_json) { registration_data_json_without_padding }
|
47
33
|
it 'does not raise "invalid base64" exception' do
|
48
34
|
expect {
|
49
35
|
register_response
|
@@ -1,16 +1,15 @@
|
|
1
1
|
require 'spec_helper.rb'
|
2
2
|
|
3
3
|
describe U2F::SignResponse do
|
4
|
-
let(:
|
5
|
-
|
6
|
-
|
7
|
-
let(:sign_response)
|
8
|
-
|
9
|
-
end
|
4
|
+
let(:app_id) { 'http://demo.example.com' }
|
5
|
+
let(:challenge) { U2F.urlsafe_encode64(SecureRandom.random_bytes(32)) }
|
6
|
+
let(:device) { U2F::FakeU2F.new(app_id) }
|
7
|
+
let(:json_response) { device.sign_response(challenge) }
|
8
|
+
let(:sign_response) { U2F::SignResponse.load_from_json json_response }
|
10
9
|
|
11
10
|
describe '#counter' do
|
12
11
|
subject { sign_response.counter }
|
13
|
-
it { is_expected.to be
|
12
|
+
it { is_expected.to be device.counter }
|
14
13
|
end
|
15
14
|
|
16
15
|
describe '#user_present?' do
|
data/spec/lib/u2f_spec.rb
CHANGED
@@ -1,35 +1,27 @@
|
|
1
|
-
require 'spec_helper'
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe U2F do
|
4
4
|
let(:app_id) { 'http://demo.example.com' }
|
5
|
+
let(:device_challenge) { U2F.urlsafe_encode64(SecureRandom.random_bytes(32)) }
|
6
|
+
let(:auth_challenge) { device_challenge }
|
5
7
|
let(:u2f) { U2F::U2F.new(app_id) }
|
6
|
-
let(:
|
7
|
-
let(:key_handle)
|
8
|
-
|
9
|
-
|
10
|
-
let(:
|
11
|
-
|
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
|
-
U2F.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
|
8
|
+
let(:device) { U2F::FakeU2F.new(app_id) }
|
9
|
+
let(:key_handle) { U2F.urlsafe_encode64(device.key_handle_raw) }
|
10
|
+
let(:certificate) { Base64.strict_encode64(device.cert_raw) }
|
11
|
+
let(:public_key) { device.origin_public_key_raw }
|
12
|
+
let(:register_response_json) { device.register_response(device_challenge) }
|
13
|
+
let(:sign_response_json) { device.sign_response(device_challenge) }
|
22
14
|
let(:registration) do
|
23
15
|
U2F::Registration.new(key_handle, public_key, certificate)
|
24
16
|
end
|
25
17
|
let(:register_response) do
|
26
|
-
U2F::RegisterResponse.load_from_json(
|
18
|
+
U2F::RegisterResponse.load_from_json(register_response_json)
|
27
19
|
end
|
28
|
-
let(:
|
29
|
-
U2F::SignResponse.load_from_json
|
20
|
+
let(:sign_response) do
|
21
|
+
U2F::SignResponse.load_from_json sign_response_json
|
30
22
|
end
|
31
23
|
let(:sign_request) do
|
32
|
-
U2F::SignRequest.new(key_handle,
|
24
|
+
U2F::SignRequest.new(key_handle, auth_challenge, app_id)
|
33
25
|
end
|
34
26
|
|
35
27
|
describe '#authentication_requests' do
|
@@ -44,7 +36,7 @@ describe U2F do
|
|
44
36
|
let(:counter) { registration.counter }
|
45
37
|
let(:reg_public_key) { registration.public_key }
|
46
38
|
let (:u2f_authenticate) do
|
47
|
-
u2f.authenticate!(
|
39
|
+
u2f.authenticate!(auth_challenge, sign_response, reg_public_key, counter)
|
48
40
|
end
|
49
41
|
context 'with correct parameters' do
|
50
42
|
it 'does not raise an error' do
|
@@ -53,7 +45,7 @@ describe U2F do
|
|
53
45
|
end
|
54
46
|
|
55
47
|
context 'with incorrect challenge' do
|
56
|
-
let(:
|
48
|
+
let(:auth_challenge) { 'incorrect' }
|
57
49
|
it 'raises NoMatchingRequestError' do
|
58
50
|
expect { u2f_authenticate }.to raise_error(U2F::NoMatchingRequestError)
|
59
51
|
end
|
@@ -82,27 +74,26 @@ describe U2F do
|
|
82
74
|
end
|
83
75
|
|
84
76
|
describe '#register!' do
|
85
|
-
let(:challenge) { 'yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8' }
|
86
77
|
context 'with correct registration data' do
|
87
78
|
it 'returns a registration' do
|
88
79
|
reg = nil
|
89
80
|
expect {
|
90
|
-
reg = u2f.register!(
|
81
|
+
reg = u2f.register!(auth_challenge, register_response)
|
91
82
|
}.to_not raise_error
|
92
83
|
expect(reg.key_handle).to eq key_handle
|
93
84
|
end
|
94
85
|
|
95
86
|
it 'accepts an array of challenges' do
|
96
|
-
reg = u2f.register!(['another-challenge',
|
87
|
+
reg = u2f.register!(['another-challenge', auth_challenge], register_response)
|
97
88
|
expect(reg).to be_a U2F::Registration
|
98
89
|
end
|
99
90
|
end
|
100
91
|
|
101
92
|
context 'with unknown challenge' do
|
102
|
-
let(:
|
93
|
+
let(:auth_challenge) { 'non-matching' }
|
103
94
|
it 'raises an UnmatchedChallengeError' do
|
104
95
|
expect {
|
105
|
-
u2f.register!(
|
96
|
+
u2f.register!(auth_challenge, register_response)
|
106
97
|
}.to raise_error(U2F::UnmatchedChallengeError)
|
107
98
|
end
|
108
99
|
end
|
@@ -117,8 +108,17 @@ describe U2F do
|
|
117
108
|
end
|
118
109
|
end
|
119
110
|
|
120
|
-
context 'with
|
121
|
-
let(:public_key) { U2F.urlsafe_decode64('
|
111
|
+
context 'with invalid key' do
|
112
|
+
let(:public_key) { U2F.urlsafe_decode64('YV6FVSmH0ObY1cBRCsYJZ/CXF1gKsL+DW46rMfpeymtDZted2Ut2BraszUK1wg1+YJ4Bxt6r24WHNUYqKgeaSq8=') }
|
113
|
+
it 'fails when first byte of the key is not 0x04' do
|
114
|
+
expect {
|
115
|
+
U2F::U2F.public_key_pem public_key
|
116
|
+
}.to raise_error(U2F::PublicKeyDecodeError)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'with truncated key' do
|
121
|
+
let(:public_key) { U2F.urlsafe_decode64('BJhSPkR3Rmgl') }
|
122
122
|
it 'fails when key is to short' do
|
123
123
|
expect {
|
124
124
|
U2F::U2F.public_key_pem public_key
|
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.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Johan Brissmyr
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-06-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -107,6 +107,7 @@ files:
|
|
107
107
|
- lib/u2f.rb
|
108
108
|
- lib/u2f/client_data.rb
|
109
109
|
- lib/u2f/errors.rb
|
110
|
+
- lib/u2f/fake_u2f.rb
|
110
111
|
- lib/u2f/register_request.rb
|
111
112
|
- lib/u2f/register_response.rb
|
112
113
|
- lib/u2f/registration.rb
|