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,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module WebAuthn
|
|
6
|
+
class CredentialOptions
|
|
7
|
+
CHALLENGE_LENGTH = 32
|
|
8
|
+
|
|
9
|
+
def challenge
|
|
10
|
+
@challenge ||= SecureRandom.random_bytes(CHALLENGE_LENGTH)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def timeout
|
|
14
|
+
@timeout = WebAuthn.configuration.credential_options_timeout
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/credential_options"
|
|
4
|
+
|
|
5
|
+
module WebAuthn
|
|
6
|
+
def self.credential_request_options
|
|
7
|
+
warn(
|
|
8
|
+
"DEPRECATION WARNING: `WebAuthn.credential_request_options` is deprecated."\
|
|
9
|
+
" Please use `WebAuthn::Credential.options_for_get` instead."
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
CredentialRequestOptions.new.to_h
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class CredentialRequestOptions < CredentialOptions
|
|
16
|
+
attr_accessor :allow_credentials, :extensions, :user_verification
|
|
17
|
+
|
|
18
|
+
def initialize(allow_credentials: [], extensions: nil, user_verification: nil)
|
|
19
|
+
super()
|
|
20
|
+
|
|
21
|
+
@allow_credentials = allow_credentials
|
|
22
|
+
@extensions = extensions
|
|
23
|
+
@user_verification = user_verification
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
options = {
|
|
28
|
+
challenge: challenge,
|
|
29
|
+
timeout: timeout,
|
|
30
|
+
allowCredentials: allow_credentials
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if extensions
|
|
34
|
+
options[:extensions] = extensions
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if user_verification
|
|
38
|
+
options[:userVerification] = user_verification
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
options
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/credential_entity"
|
|
4
|
+
|
|
5
|
+
module WebAuthn
|
|
6
|
+
class CredentialUserEntity < CredentialEntity
|
|
7
|
+
attr_reader :id, :display_name
|
|
8
|
+
|
|
9
|
+
def initialize(id:, display_name: nil, **keyword_arguments)
|
|
10
|
+
super(**keyword_arguments)
|
|
11
|
+
|
|
12
|
+
@id = id
|
|
13
|
+
@display_name = display_name || name
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/encoders"
|
|
4
|
+
|
|
5
|
+
module WebAuthn
|
|
6
|
+
class Encoder
|
|
7
|
+
extend Forwardable
|
|
8
|
+
|
|
9
|
+
# https://www.w3.org/TR/webauthn-2/#base64url-encoding
|
|
10
|
+
STANDARD_ENCODING = :base64url
|
|
11
|
+
|
|
12
|
+
def_delegators :@encoder_klass, :encode, :decode
|
|
13
|
+
|
|
14
|
+
def initialize(encoding = STANDARD_ENCODING)
|
|
15
|
+
@encoder_klass = Encoders.lookup(encoding)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebAuthn
|
|
4
|
+
def self.standard_encoder
|
|
5
|
+
@standard_encoder ||= Encoders.lookup(Encoder::STANDARD_ENCODING)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module Encoders
|
|
9
|
+
class << self
|
|
10
|
+
def lookup(encoding)
|
|
11
|
+
case encoding
|
|
12
|
+
when :base64
|
|
13
|
+
Base64Encoder
|
|
14
|
+
when :base64url
|
|
15
|
+
Base64UrlEncoder
|
|
16
|
+
when nil, false
|
|
17
|
+
NullEncoder
|
|
18
|
+
else
|
|
19
|
+
raise "Unsupported or unknown encoding: #{encoding}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Base64Encoder
|
|
25
|
+
def self.encode(data)
|
|
26
|
+
[data].pack("m0") # Base64.strict_encode64(data)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.decode(data)
|
|
30
|
+
data.unpack1("m0") # Base64.strict_decode64(data)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Base64UrlEncoder
|
|
35
|
+
def self.encode(data)
|
|
36
|
+
data = [data].pack("m0") # Base64.urlsafe_encode64(data, padding: false)
|
|
37
|
+
data.chomp!("==") or data.chomp!("=")
|
|
38
|
+
data.tr!("+/", "-_")
|
|
39
|
+
data
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.decode(data)
|
|
43
|
+
if !data.end_with?("=") && data.length % 4 != 0 # Base64.urlsafe_decode64(data)
|
|
44
|
+
data = data.ljust((data.length + 3) & ~3, "=")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
data = data.tr("-_", "+/")
|
|
48
|
+
data.unpack1("m0")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class NullEncoder
|
|
53
|
+
def self.encode(data)
|
|
54
|
+
data
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.decode(data)
|
|
58
|
+
data
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/cbor"
|
|
4
|
+
require "webauthn/fake_authenticator/authenticator_data"
|
|
5
|
+
|
|
6
|
+
module WebAuthn
|
|
7
|
+
class FakeAuthenticator
|
|
8
|
+
class AttestationObject
|
|
9
|
+
def initialize(
|
|
10
|
+
client_data_hash:,
|
|
11
|
+
rp_id_hash:,
|
|
12
|
+
credential_id:,
|
|
13
|
+
credential_key:,
|
|
14
|
+
user_present: true,
|
|
15
|
+
user_verified: false,
|
|
16
|
+
backup_eligibility: false,
|
|
17
|
+
backup_state: false,
|
|
18
|
+
attested_credential_data: true,
|
|
19
|
+
sign_count: 0,
|
|
20
|
+
extensions: nil
|
|
21
|
+
)
|
|
22
|
+
@client_data_hash = client_data_hash
|
|
23
|
+
@rp_id_hash = rp_id_hash
|
|
24
|
+
@credential_id = credential_id
|
|
25
|
+
@credential_key = credential_key
|
|
26
|
+
@user_present = user_present
|
|
27
|
+
@user_verified = user_verified
|
|
28
|
+
@backup_eligibility = backup_eligibility
|
|
29
|
+
@backup_state = backup_state
|
|
30
|
+
@attested_credential_data = attested_credential_data
|
|
31
|
+
@sign_count = sign_count
|
|
32
|
+
@extensions = extensions
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def serialize
|
|
36
|
+
CBOR.encode(
|
|
37
|
+
"fmt" => "none",
|
|
38
|
+
"attStmt" => {},
|
|
39
|
+
"authData" => authenticator_data.serialize
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
attr_reader(
|
|
46
|
+
:client_data_hash,
|
|
47
|
+
:rp_id_hash,
|
|
48
|
+
:credential_id,
|
|
49
|
+
:credential_key,
|
|
50
|
+
:user_present,
|
|
51
|
+
:user_verified,
|
|
52
|
+
:backup_eligibility,
|
|
53
|
+
:backup_state,
|
|
54
|
+
:attested_credential_data,
|
|
55
|
+
:sign_count,
|
|
56
|
+
:extensions
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def authenticator_data
|
|
60
|
+
@authenticator_data ||=
|
|
61
|
+
begin
|
|
62
|
+
credential_data =
|
|
63
|
+
if attested_credential_data
|
|
64
|
+
{ id: credential_id, public_key: credential_public_key }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
AuthenticatorData.new(
|
|
68
|
+
rp_id_hash: rp_id_hash,
|
|
69
|
+
credential: credential_data,
|
|
70
|
+
user_present: user_present,
|
|
71
|
+
user_verified: user_verified,
|
|
72
|
+
backup_eligibility: backup_eligibility,
|
|
73
|
+
backup_state: backup_state,
|
|
74
|
+
sign_count: 0,
|
|
75
|
+
extensions: extensions
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def credential_public_key
|
|
81
|
+
case credential_key
|
|
82
|
+
when OpenSSL::PKey::RSA, OpenSSL::PKey::EC
|
|
83
|
+
credential_key.public_key
|
|
84
|
+
when OpenSSL::PKey::PKey
|
|
85
|
+
OpenSSL::PKey.read(credential_key.public_to_der)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/cose"
|
|
4
|
+
require "webauthn/cbor"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module WebAuthn
|
|
8
|
+
class FakeAuthenticator
|
|
9
|
+
class AuthenticatorData
|
|
10
|
+
AAGUID = SecureRandom.random_bytes(16)
|
|
11
|
+
|
|
12
|
+
attr_reader :sign_count
|
|
13
|
+
|
|
14
|
+
def initialize(
|
|
15
|
+
rp_id_hash:,
|
|
16
|
+
credential: {
|
|
17
|
+
id: SecureRandom.random_bytes(16),
|
|
18
|
+
public_key: OpenSSL::PKey::EC.generate("prime256v1").public_key
|
|
19
|
+
},
|
|
20
|
+
sign_count: 0,
|
|
21
|
+
user_present: true,
|
|
22
|
+
user_verified: !user_present,
|
|
23
|
+
backup_eligibility: false,
|
|
24
|
+
backup_state: false,
|
|
25
|
+
aaguid: AAGUID,
|
|
26
|
+
extensions: { "fakeExtension" => "fakeExtensionValue" }
|
|
27
|
+
)
|
|
28
|
+
@rp_id_hash = rp_id_hash
|
|
29
|
+
@credential = credential
|
|
30
|
+
@sign_count = sign_count
|
|
31
|
+
@user_present = user_present
|
|
32
|
+
@user_verified = user_verified
|
|
33
|
+
@backup_eligibility = backup_eligibility
|
|
34
|
+
@backup_state = backup_state
|
|
35
|
+
@aaguid = aaguid
|
|
36
|
+
@extensions = extensions
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def serialize
|
|
40
|
+
rp_id_hash + flags + serialized_sign_count + attested_credential_data + extension_data
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
attr_reader :rp_id_hash,
|
|
46
|
+
:credential,
|
|
47
|
+
:user_present,
|
|
48
|
+
:user_verified,
|
|
49
|
+
:extensions,
|
|
50
|
+
:backup_eligibility,
|
|
51
|
+
:backup_state
|
|
52
|
+
|
|
53
|
+
def flags
|
|
54
|
+
[
|
|
55
|
+
[
|
|
56
|
+
bit(:user_present),
|
|
57
|
+
reserved_for_future_use_bit,
|
|
58
|
+
bit(:user_verified),
|
|
59
|
+
bit(:backup_eligibility),
|
|
60
|
+
bit(:backup_state),
|
|
61
|
+
reserved_for_future_use_bit,
|
|
62
|
+
attested_credential_data_included_bit,
|
|
63
|
+
extension_data_included_bit
|
|
64
|
+
].join
|
|
65
|
+
].pack("b*")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def serialized_sign_count
|
|
69
|
+
[sign_count].pack('L>')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def attested_credential_data
|
|
73
|
+
@attested_credential_data ||=
|
|
74
|
+
if credential
|
|
75
|
+
@aaguid +
|
|
76
|
+
[credential[:id].length].pack("n*") +
|
|
77
|
+
credential[:id] +
|
|
78
|
+
cose_credential_public_key
|
|
79
|
+
else
|
|
80
|
+
""
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def extension_data
|
|
85
|
+
if extensions
|
|
86
|
+
CBOR.encode(extensions)
|
|
87
|
+
else
|
|
88
|
+
""
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def bit(flag)
|
|
93
|
+
if context[flag]
|
|
94
|
+
"1"
|
|
95
|
+
else
|
|
96
|
+
"0"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def attested_credential_data_included_bit
|
|
101
|
+
if attested_credential_data.empty?
|
|
102
|
+
"0"
|
|
103
|
+
else
|
|
104
|
+
"1"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def extension_data_included_bit
|
|
109
|
+
if extension_data.empty?
|
|
110
|
+
"0"
|
|
111
|
+
else
|
|
112
|
+
"1"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def reserved_for_future_use_bit
|
|
117
|
+
"0"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def context
|
|
121
|
+
{
|
|
122
|
+
user_present: user_present,
|
|
123
|
+
user_verified: user_verified,
|
|
124
|
+
backup_eligibility: backup_eligibility,
|
|
125
|
+
backup_state: backup_state
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def cose_credential_public_key
|
|
130
|
+
case credential[:public_key]
|
|
131
|
+
when OpenSSL::PKey::RSA
|
|
132
|
+
key = COSE::Key::RSA.from_pkey(credential[:public_key])
|
|
133
|
+
key.alg = -257
|
|
134
|
+
when OpenSSL::PKey::EC::Point
|
|
135
|
+
alg = {
|
|
136
|
+
COSE::Key::Curve.by_name("P-256").id => -7,
|
|
137
|
+
COSE::Key::Curve.by_name("P-384").id => -35,
|
|
138
|
+
COSE::Key::Curve.by_name("P-521").id => -36
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
key = COSE::Key::EC2.from_pkey(credential[:public_key])
|
|
142
|
+
key.alg = alg[key.crv]
|
|
143
|
+
when OpenSSL::PKey::PKey
|
|
144
|
+
key = COSE::Key::OKP.from_pkey(credential[:public_key])
|
|
145
|
+
key.alg = -8
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
key.serialize
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def key_bytes(public_key)
|
|
152
|
+
public_key.to_bn.to_s(2)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/cbor"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "webauthn/fake_authenticator/attestation_object"
|
|
7
|
+
require "webauthn/fake_authenticator/authenticator_data"
|
|
8
|
+
|
|
9
|
+
module WebAuthn
|
|
10
|
+
class FakeAuthenticator
|
|
11
|
+
def initialize
|
|
12
|
+
@credentials = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def make_credential(
|
|
16
|
+
rp_id:,
|
|
17
|
+
client_data_hash:,
|
|
18
|
+
user_present: true,
|
|
19
|
+
user_verified: false,
|
|
20
|
+
backup_eligibility: false,
|
|
21
|
+
backup_state: false,
|
|
22
|
+
attested_credential_data: true,
|
|
23
|
+
algorithm: nil,
|
|
24
|
+
sign_count: nil,
|
|
25
|
+
extensions: nil
|
|
26
|
+
)
|
|
27
|
+
credential_id, credential_key, credential_sign_count = new_credential(algorithm)
|
|
28
|
+
sign_count ||= credential_sign_count
|
|
29
|
+
|
|
30
|
+
credentials[rp_id] ||= {}
|
|
31
|
+
credentials[rp_id][credential_id] = {
|
|
32
|
+
credential_key: credential_key,
|
|
33
|
+
sign_count: sign_count + 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
AttestationObject.new(
|
|
37
|
+
client_data_hash: client_data_hash,
|
|
38
|
+
rp_id_hash: hashed(rp_id),
|
|
39
|
+
credential_id: credential_id,
|
|
40
|
+
credential_key: credential_key,
|
|
41
|
+
user_present: user_present,
|
|
42
|
+
user_verified: user_verified,
|
|
43
|
+
backup_eligibility: backup_eligibility,
|
|
44
|
+
backup_state: backup_state,
|
|
45
|
+
attested_credential_data: attested_credential_data,
|
|
46
|
+
sign_count: sign_count,
|
|
47
|
+
extensions: extensions
|
|
48
|
+
).serialize
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def get_assertion(
|
|
52
|
+
rp_id:,
|
|
53
|
+
client_data_hash:,
|
|
54
|
+
user_present: true,
|
|
55
|
+
user_verified: false,
|
|
56
|
+
backup_eligibility: false,
|
|
57
|
+
backup_state: false,
|
|
58
|
+
aaguid: AuthenticatorData::AAGUID,
|
|
59
|
+
sign_count: nil,
|
|
60
|
+
extensions: nil,
|
|
61
|
+
allow_credentials: nil
|
|
62
|
+
)
|
|
63
|
+
credential_options = credentials[rp_id]
|
|
64
|
+
|
|
65
|
+
if credential_options
|
|
66
|
+
allow_credentials ||= credential_options.keys
|
|
67
|
+
credential_id = (credential_options.keys & allow_credentials).first
|
|
68
|
+
unless credential_id
|
|
69
|
+
raise "No matching credentials (allowed=#{allow_credentials}) " \
|
|
70
|
+
"found for RP #{rp_id} among credentials=#{credential_options}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
credential = credential_options[credential_id]
|
|
74
|
+
credential_key = credential[:credential_key]
|
|
75
|
+
credential_sign_count = credential[:sign_count]
|
|
76
|
+
|
|
77
|
+
authenticator_data = AuthenticatorData.new(
|
|
78
|
+
rp_id_hash: hashed(rp_id),
|
|
79
|
+
user_present: user_present,
|
|
80
|
+
user_verified: user_verified,
|
|
81
|
+
backup_eligibility: backup_eligibility,
|
|
82
|
+
backup_state: backup_state,
|
|
83
|
+
aaguid: aaguid,
|
|
84
|
+
credential: nil,
|
|
85
|
+
sign_count: sign_count || credential_sign_count,
|
|
86
|
+
extensions: extensions
|
|
87
|
+
).serialize
|
|
88
|
+
|
|
89
|
+
signature_digest_algorithm =
|
|
90
|
+
case credential_key
|
|
91
|
+
when OpenSSL::PKey::RSA, OpenSSL::PKey::EC
|
|
92
|
+
'SHA256'
|
|
93
|
+
when OpenSSL::PKey::PKey
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
signature = credential_key.sign(signature_digest_algorithm, authenticator_data + client_data_hash)
|
|
97
|
+
credential[:sign_count] += 1
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
credential_id: credential_id,
|
|
101
|
+
authenticator_data: authenticator_data,
|
|
102
|
+
signature: signature
|
|
103
|
+
}
|
|
104
|
+
else
|
|
105
|
+
raise "No credentials found for RP #{rp_id}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
attr_reader :credentials
|
|
112
|
+
|
|
113
|
+
def new_credential(algorithm)
|
|
114
|
+
algorithm ||= 'ES256'
|
|
115
|
+
credential_key =
|
|
116
|
+
case algorithm
|
|
117
|
+
when 'ES256'
|
|
118
|
+
OpenSSL::PKey::EC.generate('prime256v1')
|
|
119
|
+
when 'RS256'
|
|
120
|
+
OpenSSL::PKey::RSA.new(2048)
|
|
121
|
+
when 'EdDSA'
|
|
122
|
+
OpenSSL::PKey.generate_key("ED25519")
|
|
123
|
+
else
|
|
124
|
+
raise "Unsupported algorithm #{algorithm}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
[SecureRandom.random_bytes(16), credential_key, 0]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def hashed(target)
|
|
131
|
+
OpenSSL::Digest::SHA256.digest(target)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|