webauthn-slim 3.4.3
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 +7 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/actions/install-openssl/action.yml +35 -0
- data/.github/actions/install-ruby/action.yml +76 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/build.yml +49 -0
- data/.github/workflows/git.yml +21 -0
- data/.github/workflows/release.yml +28 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.rubocop.yml +306 -0
- data/CHANGELOG.md +462 -0
- data/CONTRIBUTING.md +39 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +479 -0
- data/Rakefile +10 -0
- data/SECURITY.md +21 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/docs/advanced_configuration.md +173 -0
- data/docs/u2f_migration.md +90 -0
- data/lib/webauthn/attestation_object.rb +47 -0
- data/lib/webauthn/attestation_statement/apple.rb +65 -0
- data/lib/webauthn/attestation_statement/base.rb +175 -0
- data/lib/webauthn/attestation_statement/fido_u2f/public_key.rb +44 -0
- data/lib/webauthn/attestation_statement/fido_u2f.rb +71 -0
- data/lib/webauthn/attestation_statement/none.rb +23 -0
- data/lib/webauthn/attestation_statement/packed.rb +72 -0
- data/lib/webauthn/attestation_statement.rb +35 -0
- data/lib/webauthn/authenticator_assertion_response.rb +87 -0
- data/lib/webauthn/authenticator_attestation_response.rb +84 -0
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +94 -0
- data/lib/webauthn/authenticator_data.rb +130 -0
- data/lib/webauthn/authenticator_response.rb +137 -0
- data/lib/webauthn/cbor.rb +189 -0
- data/lib/webauthn/client_data.rb +67 -0
- data/lib/webauthn/configuration.rb +68 -0
- data/lib/webauthn/cose.rb +237 -0
- data/lib/webauthn/credential.rb +26 -0
- data/lib/webauthn/credential_creation_options.rb +97 -0
- data/lib/webauthn/credential_entity.rb +11 -0
- data/lib/webauthn/credential_options.rb +17 -0
- data/lib/webauthn/credential_request_options.rb +44 -0
- data/lib/webauthn/credential_rp_entity.rb +8 -0
- data/lib/webauthn/credential_user_entity.rb +16 -0
- data/lib/webauthn/encoder.rb +18 -0
- data/lib/webauthn/encoders.rb +62 -0
- data/lib/webauthn/error.rb +5 -0
- data/lib/webauthn/fake_authenticator/attestation_object.rb +90 -0
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +156 -0
- data/lib/webauthn/fake_authenticator.rb +134 -0
- data/lib/webauthn/fake_client.rb +179 -0
- data/lib/webauthn/json_serializer.rb +45 -0
- data/lib/webauthn/public_key.rb +65 -0
- data/lib/webauthn/public_key_credential/creation_options.rb +93 -0
- data/lib/webauthn/public_key_credential/entity.rb +21 -0
- data/lib/webauthn/public_key_credential/options.rb +51 -0
- data/lib/webauthn/public_key_credential/request_options.rb +46 -0
- data/lib/webauthn/public_key_credential/rp_entity.rb +23 -0
- data/lib/webauthn/public_key_credential/user_entity.rb +24 -0
- data/lib/webauthn/public_key_credential.rb +94 -0
- data/lib/webauthn/public_key_credential_with_assertion.rb +49 -0
- data/lib/webauthn/public_key_credential_with_attestation.rb +30 -0
- data/lib/webauthn/relying_party.rb +150 -0
- data/lib/webauthn/u2f_migrator.rb +76 -0
- data/lib/webauthn/version.rb +5 -0
- data/lib/webauthn.rb +19 -0
- data/webauthn.gemspec +43 -0
- metadata +218 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "webauthn/authenticator_data"
|
|
6
|
+
require "webauthn/encoder"
|
|
7
|
+
require "webauthn/fake_authenticator"
|
|
8
|
+
|
|
9
|
+
module WebAuthn
|
|
10
|
+
class FakeClient
|
|
11
|
+
TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :origin, :cross_origin, :top_origin, :token_binding, :encoding
|
|
14
|
+
|
|
15
|
+
def initialize(
|
|
16
|
+
origin = fake_origin,
|
|
17
|
+
cross_origin: nil,
|
|
18
|
+
top_origin: nil,
|
|
19
|
+
token_binding: nil,
|
|
20
|
+
authenticator: WebAuthn::FakeAuthenticator.new,
|
|
21
|
+
encoding: WebAuthn.configuration.encoding
|
|
22
|
+
)
|
|
23
|
+
@origin = origin
|
|
24
|
+
@cross_origin = cross_origin
|
|
25
|
+
@top_origin = top_origin
|
|
26
|
+
@token_binding = token_binding
|
|
27
|
+
@authenticator = authenticator
|
|
28
|
+
@encoding = encoding
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create(
|
|
32
|
+
challenge: fake_challenge,
|
|
33
|
+
rp_id: nil,
|
|
34
|
+
user_present: true,
|
|
35
|
+
user_verified: false,
|
|
36
|
+
backup_eligibility: false,
|
|
37
|
+
backup_state: false,
|
|
38
|
+
attested_credential_data: true,
|
|
39
|
+
credential_algorithm: nil,
|
|
40
|
+
extensions: nil
|
|
41
|
+
)
|
|
42
|
+
rp_id ||= URI.parse(origin).host
|
|
43
|
+
|
|
44
|
+
client_data_json = data_json_for(:create, encoder.decode(challenge))
|
|
45
|
+
client_data_hash = hashed(client_data_json)
|
|
46
|
+
|
|
47
|
+
attestation_object = authenticator.make_credential(
|
|
48
|
+
rp_id: rp_id,
|
|
49
|
+
client_data_hash: client_data_hash,
|
|
50
|
+
user_present: user_present,
|
|
51
|
+
user_verified: user_verified,
|
|
52
|
+
backup_eligibility: backup_eligibility,
|
|
53
|
+
backup_state: backup_state,
|
|
54
|
+
attested_credential_data: attested_credential_data,
|
|
55
|
+
algorithm: credential_algorithm,
|
|
56
|
+
extensions: extensions
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
id =
|
|
60
|
+
if attested_credential_data
|
|
61
|
+
WebAuthn::AuthenticatorData
|
|
62
|
+
.deserialize(CBOR.decode(attestation_object)["authData"])
|
|
63
|
+
.attested_credential_data
|
|
64
|
+
.id
|
|
65
|
+
else
|
|
66
|
+
"id-for-pk-without-attested-credential-data"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
"type" => "public-key",
|
|
71
|
+
"id" => internal_encoder.encode(id),
|
|
72
|
+
"rawId" => encoder.encode(id),
|
|
73
|
+
"authenticatorAttachment" => 'platform',
|
|
74
|
+
"clientExtensionResults" => extensions,
|
|
75
|
+
"response" => {
|
|
76
|
+
"attestationObject" => encoder.encode(attestation_object),
|
|
77
|
+
"clientDataJSON" => encoder.encode(client_data_json),
|
|
78
|
+
"transports" => ["internal"],
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def get(challenge: fake_challenge,
|
|
84
|
+
rp_id: nil,
|
|
85
|
+
user_present: true,
|
|
86
|
+
user_verified: false,
|
|
87
|
+
backup_eligibility: false,
|
|
88
|
+
backup_state: true,
|
|
89
|
+
sign_count: nil,
|
|
90
|
+
extensions: nil,
|
|
91
|
+
user_handle: nil,
|
|
92
|
+
allow_credentials: nil)
|
|
93
|
+
rp_id ||= URI.parse(origin).host
|
|
94
|
+
|
|
95
|
+
client_data_json = data_json_for(:get, encoder.decode(challenge))
|
|
96
|
+
client_data_hash = hashed(client_data_json)
|
|
97
|
+
|
|
98
|
+
if allow_credentials
|
|
99
|
+
allow_credentials = allow_credentials.map { |credential| encoder.decode(credential) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
assertion = authenticator.get_assertion(
|
|
103
|
+
rp_id: rp_id,
|
|
104
|
+
client_data_hash: client_data_hash,
|
|
105
|
+
user_present: user_present,
|
|
106
|
+
user_verified: user_verified,
|
|
107
|
+
backup_eligibility: backup_eligibility,
|
|
108
|
+
backup_state: backup_state,
|
|
109
|
+
sign_count: sign_count,
|
|
110
|
+
extensions: extensions,
|
|
111
|
+
allow_credentials: allow_credentials
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
{
|
|
115
|
+
"type" => "public-key",
|
|
116
|
+
"id" => internal_encoder.encode(assertion[:credential_id]),
|
|
117
|
+
"rawId" => encoder.encode(assertion[:credential_id]),
|
|
118
|
+
"clientExtensionResults" => extensions,
|
|
119
|
+
"authenticatorAttachment" => 'platform',
|
|
120
|
+
"response" => {
|
|
121
|
+
"clientDataJSON" => encoder.encode(client_data_json),
|
|
122
|
+
"authenticatorData" => encoder.encode(assertion[:authenticator_data]),
|
|
123
|
+
"signature" => encoder.encode(assertion[:signature]),
|
|
124
|
+
"userHandle" => user_handle ? encoder.encode(user_handle) : nil
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
attr_reader :authenticator
|
|
132
|
+
|
|
133
|
+
def data_json_for(method, challenge)
|
|
134
|
+
data = {
|
|
135
|
+
type: type_for(method),
|
|
136
|
+
challenge: internal_encoder.encode(challenge),
|
|
137
|
+
origin: origin
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if token_binding
|
|
141
|
+
data[:tokenBinding] = token_binding
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if cross_origin
|
|
145
|
+
data[:crossOrigin] = cross_origin
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
if top_origin
|
|
149
|
+
data[:topOrigin] = top_origin
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
data.to_json
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def encoder
|
|
156
|
+
@encoder ||= WebAuthn::Encoder.new(encoding)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def internal_encoder
|
|
160
|
+
WebAuthn.standard_encoder
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def hashed(data)
|
|
164
|
+
OpenSSL::Digest::SHA256.digest(data)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def fake_challenge
|
|
168
|
+
encoder.encode(SecureRandom.random_bytes(32))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def fake_origin
|
|
172
|
+
"http://localhost#{rand(1000)}.test"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def type_for(method)
|
|
176
|
+
TYPES[method]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebAuthn
|
|
4
|
+
module JSONSerializer
|
|
5
|
+
# Argument wildcard for Ruby on Rails controller automatic object JSON serialization
|
|
6
|
+
def as_json(*)
|
|
7
|
+
deep_camelize_keys(to_hash)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def to_hash
|
|
13
|
+
attributes.each_with_object({}) do |attribute_name, hash|
|
|
14
|
+
value = send(attribute_name)
|
|
15
|
+
|
|
16
|
+
if value.respond_to?(:as_json)
|
|
17
|
+
value = value.as_json
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if value
|
|
21
|
+
hash[attribute_name] = value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def deep_camelize_keys(object)
|
|
27
|
+
case object
|
|
28
|
+
when Hash
|
|
29
|
+
object.each_with_object({}) do |(key, value), result|
|
|
30
|
+
result[camelize(key)] = deep_camelize_keys(value)
|
|
31
|
+
end
|
|
32
|
+
when Array
|
|
33
|
+
object.map { |element| deep_camelize_keys(element) }
|
|
34
|
+
else
|
|
35
|
+
object
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def camelize(term)
|
|
40
|
+
first_term, *rest = term.to_s.split('_')
|
|
41
|
+
|
|
42
|
+
[first_term, *rest.map(&:capitalize)].join.to_sym
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/cose"
|
|
4
|
+
require "webauthn/attestation_statement/fido_u2f/public_key"
|
|
5
|
+
|
|
6
|
+
module WebAuthn
|
|
7
|
+
class PublicKey
|
|
8
|
+
class UnsupportedAlgorithm < Error; end
|
|
9
|
+
|
|
10
|
+
def self.deserialize(public_key)
|
|
11
|
+
cose_key =
|
|
12
|
+
if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(public_key)
|
|
13
|
+
# Gem version v1.11.0 and lower, used to behave so that Credential#public_key
|
|
14
|
+
# returned an EC P-256 uncompressed point.
|
|
15
|
+
#
|
|
16
|
+
# Because of https://github.com/cedarcode/webauthn-ruby/issues/137 this was changed
|
|
17
|
+
# and Credential#public_key started returning the unchanged COSE_Key formatted
|
|
18
|
+
# credentialPublicKey (as in https://www.w3.org/TR/webauthn/#credentialpublickey).
|
|
19
|
+
#
|
|
20
|
+
# Given that the credential public key is expected to be stored long-term by the gem
|
|
21
|
+
# user and later be passed as the public_key argument in the
|
|
22
|
+
# AuthenticatorAssertionResponse.verify call, we then need to support the two formats.
|
|
23
|
+
COSE::Key::EC2.new(
|
|
24
|
+
alg: COSE::Algorithm.by_name("ES256").id,
|
|
25
|
+
crv: 1,
|
|
26
|
+
x: public_key[1..32],
|
|
27
|
+
y: public_key[33..-1]
|
|
28
|
+
)
|
|
29
|
+
else
|
|
30
|
+
COSE::Key.deserialize(public_key)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
new(cose_key: cose_key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :cose_key
|
|
37
|
+
|
|
38
|
+
def initialize(cose_key:)
|
|
39
|
+
@cose_key = cose_key
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def pkey
|
|
43
|
+
@cose_key.to_pkey
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def alg
|
|
47
|
+
@cose_key.alg
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def verify(signature, verification_data)
|
|
51
|
+
cose_algorithm.verify(pkey, signature, verification_data)
|
|
52
|
+
rescue COSE::Error
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def cose_algorithm
|
|
59
|
+
@cose_algorithm ||= COSE::Algorithm.find(alg) || raise(
|
|
60
|
+
UnsupportedAlgorithm,
|
|
61
|
+
"The public key algorithm #{alg} is not among the available COSE algorithms"
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/cose"
|
|
4
|
+
require "webauthn/public_key_credential/options"
|
|
5
|
+
require "webauthn/public_key_credential/rp_entity"
|
|
6
|
+
require "webauthn/public_key_credential/user_entity"
|
|
7
|
+
|
|
8
|
+
module WebAuthn
|
|
9
|
+
class PublicKeyCredential
|
|
10
|
+
class CreationOptions < Options
|
|
11
|
+
attr_accessor(
|
|
12
|
+
:attestation,
|
|
13
|
+
:authenticator_selection,
|
|
14
|
+
:exclude,
|
|
15
|
+
:algs,
|
|
16
|
+
:rp,
|
|
17
|
+
:user
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def initialize(
|
|
21
|
+
attestation: nil,
|
|
22
|
+
authenticator_selection: nil,
|
|
23
|
+
exclude_credentials: nil,
|
|
24
|
+
exclude: nil,
|
|
25
|
+
pub_key_cred_params: nil,
|
|
26
|
+
algs: nil,
|
|
27
|
+
rp: {},
|
|
28
|
+
user:,
|
|
29
|
+
**keyword_arguments
|
|
30
|
+
)
|
|
31
|
+
super(**keyword_arguments)
|
|
32
|
+
|
|
33
|
+
@attestation = attestation
|
|
34
|
+
@authenticator_selection = authenticator_selection
|
|
35
|
+
@exclude_credentials = exclude_credentials
|
|
36
|
+
@exclude = exclude
|
|
37
|
+
@pub_key_cred_params = pub_key_cred_params
|
|
38
|
+
@algs = algs
|
|
39
|
+
|
|
40
|
+
@rp =
|
|
41
|
+
if rp.is_a?(Hash)
|
|
42
|
+
rp = rp.dup
|
|
43
|
+
rp[:name] ||= relying_party.name
|
|
44
|
+
rp[:id] ||= relying_party.id
|
|
45
|
+
|
|
46
|
+
RPEntity.new(**rp)
|
|
47
|
+
else
|
|
48
|
+
rp
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@user =
|
|
52
|
+
if user.is_a?(Hash)
|
|
53
|
+
UserEntity.new(**user)
|
|
54
|
+
else
|
|
55
|
+
user
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def exclude_credentials
|
|
60
|
+
@exclude_credentials || exclude_credentials_from_exclude
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def pub_key_cred_params
|
|
64
|
+
@pub_key_cred_params || pub_key_cred_params_from_algs
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def attributes
|
|
70
|
+
super.concat([:rp, :user, :pub_key_cred_params, :attestation, :authenticator_selection, :exclude_credentials])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def exclude_credentials_from_exclude
|
|
74
|
+
if exclude
|
|
75
|
+
as_public_key_descriptors(exclude)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def pub_key_cred_params_from_algs
|
|
80
|
+
Array(algs || relying_party.algorithms).map do |alg|
|
|
81
|
+
alg_id =
|
|
82
|
+
if alg.is_a?(String) || alg.is_a?(Symbol)
|
|
83
|
+
COSE::Algorithm.by_name(alg.to_s).id
|
|
84
|
+
else
|
|
85
|
+
alg
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
{ type: TYPE_PUBLIC_KEY, alg: alg_id }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebAuthn
|
|
4
|
+
class PublicKeyCredential
|
|
5
|
+
class Entity
|
|
6
|
+
include JSONSerializer
|
|
7
|
+
|
|
8
|
+
attr_reader :name
|
|
9
|
+
|
|
10
|
+
def initialize(name:)
|
|
11
|
+
@name = name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def attributes
|
|
17
|
+
[:name]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module WebAuthn
|
|
6
|
+
class PublicKeyCredential
|
|
7
|
+
class Options
|
|
8
|
+
include JSONSerializer
|
|
9
|
+
|
|
10
|
+
CHALLENGE_LENGTH = 32
|
|
11
|
+
|
|
12
|
+
attr_reader :timeout, :extensions, :relying_party
|
|
13
|
+
|
|
14
|
+
def initialize(timeout: nil, extensions: nil, relying_party: WebAuthn.configuration.relying_party)
|
|
15
|
+
@relying_party = relying_party
|
|
16
|
+
@timeout = timeout || default_timeout
|
|
17
|
+
@extensions = default_extensions.merge(extensions || {})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def challenge
|
|
21
|
+
encoder.encode(raw_challenge)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def attributes
|
|
27
|
+
[:challenge, :timeout, :extensions]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def encoder
|
|
31
|
+
relying_party.encoder
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def raw_challenge
|
|
35
|
+
@raw_challenge ||= SecureRandom.random_bytes(CHALLENGE_LENGTH)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def default_timeout
|
|
39
|
+
relying_party.credential_options_timeout
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def default_extensions
|
|
43
|
+
{}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def as_public_key_descriptors(ids)
|
|
47
|
+
Array(ids).map { |id| { type: TYPE_PUBLIC_KEY, id: id } }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/public_key_credential/options"
|
|
4
|
+
|
|
5
|
+
module WebAuthn
|
|
6
|
+
class PublicKeyCredential
|
|
7
|
+
class RequestOptions < Options
|
|
8
|
+
attr_accessor :rp_id, :allow, :user_verification
|
|
9
|
+
|
|
10
|
+
def initialize(rp_id: nil, allow_credentials: nil, allow: nil, user_verification: nil, **keyword_arguments)
|
|
11
|
+
super(**keyword_arguments)
|
|
12
|
+
|
|
13
|
+
@rp_id = rp_id || relying_party.id
|
|
14
|
+
@allow_credentials = allow_credentials
|
|
15
|
+
@allow = allow
|
|
16
|
+
@user_verification = user_verification
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def allow_credentials
|
|
20
|
+
@allow_credentials || allow_credentials_from_allow || []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def attributes
|
|
26
|
+
super.concat([:allow_credentials, :rp_id, :user_verification])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def default_extensions
|
|
30
|
+
extensions = super || {}
|
|
31
|
+
|
|
32
|
+
if relying_party.legacy_u2f_appid
|
|
33
|
+
extensions.merge!(appid: relying_party.legacy_u2f_appid)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
extensions
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def allow_credentials_from_allow
|
|
40
|
+
if allow
|
|
41
|
+
as_public_key_descriptors(allow)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/public_key_credential/entity"
|
|
4
|
+
|
|
5
|
+
module WebAuthn
|
|
6
|
+
class PublicKeyCredential
|
|
7
|
+
class RPEntity < Entity
|
|
8
|
+
attr_reader :id
|
|
9
|
+
|
|
10
|
+
def initialize(id: nil, **keyword_arguments)
|
|
11
|
+
super(**keyword_arguments)
|
|
12
|
+
|
|
13
|
+
@id = id
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def attributes
|
|
19
|
+
super.concat([:id])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/public_key_credential/entity"
|
|
4
|
+
|
|
5
|
+
module WebAuthn
|
|
6
|
+
class PublicKeyCredential
|
|
7
|
+
class UserEntity < Entity
|
|
8
|
+
attr_reader :id, :display_name
|
|
9
|
+
|
|
10
|
+
def initialize(id:, display_name: nil, **keyword_arguments)
|
|
11
|
+
super(**keyword_arguments)
|
|
12
|
+
|
|
13
|
+
@id = id
|
|
14
|
+
@display_name = display_name || name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def attributes
|
|
20
|
+
super.concat([:id, :display_name])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/encoder"
|
|
4
|
+
|
|
5
|
+
module WebAuthn
|
|
6
|
+
class PublicKeyCredential
|
|
7
|
+
class InvalidChallengeError < Error; end
|
|
8
|
+
|
|
9
|
+
attr_reader :type, :id, :raw_id, :client_extension_outputs, :authenticator_attachment, :response
|
|
10
|
+
|
|
11
|
+
def self.from_client(credential, relying_party: WebAuthn.configuration.relying_party)
|
|
12
|
+
new(
|
|
13
|
+
type: credential["type"],
|
|
14
|
+
id: credential["id"],
|
|
15
|
+
raw_id: relying_party.encoder.decode(credential["rawId"]),
|
|
16
|
+
client_extension_outputs: credential["clientExtensionResults"],
|
|
17
|
+
authenticator_attachment: credential["authenticatorAttachment"],
|
|
18
|
+
response: response_class.from_client(credential["response"], relying_party: relying_party),
|
|
19
|
+
relying_party: relying_party
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(
|
|
24
|
+
type:,
|
|
25
|
+
id:,
|
|
26
|
+
raw_id:,
|
|
27
|
+
response:,
|
|
28
|
+
authenticator_attachment: nil,
|
|
29
|
+
client_extension_outputs: {},
|
|
30
|
+
relying_party: WebAuthn.configuration.relying_party
|
|
31
|
+
)
|
|
32
|
+
@type = type
|
|
33
|
+
@id = id
|
|
34
|
+
@raw_id = raw_id
|
|
35
|
+
@client_extension_outputs = client_extension_outputs
|
|
36
|
+
@authenticator_attachment = authenticator_attachment
|
|
37
|
+
@response = response
|
|
38
|
+
@relying_party = relying_party
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def verify(challenge, *_args)
|
|
42
|
+
unless valid_class?(challenge)
|
|
43
|
+
msg = "challenge must be a String. input challenge class: #{challenge.class}"
|
|
44
|
+
|
|
45
|
+
raise(InvalidChallengeError, msg)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
valid_type? || raise("invalid type")
|
|
49
|
+
valid_id? || raise("invalid id")
|
|
50
|
+
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def sign_count
|
|
55
|
+
authenticator_data&.sign_count
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def authenticator_extension_outputs
|
|
59
|
+
authenticator_data.extension_data if authenticator_data&.extension_data_included?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def backup_eligible?
|
|
63
|
+
authenticator_data&.credential_backup_eligible?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def backed_up?
|
|
67
|
+
authenticator_data&.credential_backed_up?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
attr_reader :relying_party
|
|
73
|
+
|
|
74
|
+
def valid_type?
|
|
75
|
+
type == TYPE_PUBLIC_KEY
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def valid_id?
|
|
79
|
+
raw_id && id && raw_id == WebAuthn.standard_encoder.decode(id)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def valid_class?(challenge)
|
|
83
|
+
challenge.is_a?(String)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def authenticator_data
|
|
87
|
+
response&.authenticator_data
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def encoder
|
|
91
|
+
relying_party.encoder
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/authenticator_assertion_response"
|
|
4
|
+
require "webauthn/public_key_credential"
|
|
5
|
+
|
|
6
|
+
module WebAuthn
|
|
7
|
+
class PublicKeyCredentialWithAssertion < PublicKeyCredential
|
|
8
|
+
def self.response_class
|
|
9
|
+
WebAuthn::AuthenticatorAssertionResponse
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def verify(challenge, public_key:, sign_count:, user_presence: nil, user_verification: nil)
|
|
13
|
+
super
|
|
14
|
+
|
|
15
|
+
response.verify(
|
|
16
|
+
encoder.decode(challenge),
|
|
17
|
+
public_key: encoder.decode(public_key),
|
|
18
|
+
sign_count: sign_count,
|
|
19
|
+
user_presence: user_presence,
|
|
20
|
+
user_verification: user_verification,
|
|
21
|
+
rp_id: appid_extension_output ? appid : nil
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def user_handle
|
|
28
|
+
if raw_user_handle
|
|
29
|
+
encoder.encode(raw_user_handle)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def raw_user_handle
|
|
34
|
+
response.user_handle
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def appid_extension_output
|
|
40
|
+
return if client_extension_outputs.nil?
|
|
41
|
+
|
|
42
|
+
client_extension_outputs['appid']
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def appid
|
|
46
|
+
URI.parse(relying_party.legacy_u2f_appid || raise("Unspecified legacy U2F AppID")).to_s
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|