u2f 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|