webauthn 2.3.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +32 -0
- data/.github/workflows/git.yml +21 -0
- data/.rubocop.yml +57 -1
- data/CHANGELOG.md +79 -0
- data/README.md +8 -5
- data/SECURITY.md +6 -3
- data/docs/advanced_configuration.md +174 -0
- data/docs/u2f_migration.md +14 -20
- data/lib/cose/rsapkcs1_algorithm.rb +7 -0
- data/lib/webauthn/attestation_object.rb +9 -5
- data/lib/webauthn/attestation_statement/android_key.rb +0 -4
- data/lib/webauthn/attestation_statement/android_safetynet.rb +1 -5
- data/lib/webauthn/attestation_statement/apple.rb +65 -0
- data/lib/webauthn/attestation_statement/base.rb +18 -32
- data/lib/webauthn/attestation_statement/none.rb +7 -1
- data/lib/webauthn/attestation_statement/packed.rb +1 -1
- data/lib/webauthn/attestation_statement/tpm.rb +2 -2
- data/lib/webauthn/attestation_statement.rb +6 -3
- data/lib/webauthn/authenticator_assertion_response.rb +4 -3
- data/lib/webauthn/authenticator_attestation_response.rb +10 -7
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +10 -5
- data/lib/webauthn/authenticator_data.rb +10 -2
- data/lib/webauthn/authenticator_response.rb +7 -7
- data/lib/webauthn/configuration.rb +38 -42
- data/lib/webauthn/credential.rb +5 -4
- data/lib/webauthn/credential_creation_options.rb +2 -0
- data/lib/webauthn/credential_request_options.rb +2 -0
- data/lib/webauthn/fake_authenticator/attestation_object.rb +8 -0
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +20 -5
- data/lib/webauthn/fake_authenticator.rb +19 -3
- data/lib/webauthn/fake_client.rb +20 -5
- data/lib/webauthn/public_key_credential/creation_options.rb +3 -3
- data/lib/webauthn/public_key_credential/entity.rb +3 -4
- data/lib/webauthn/public_key_credential/options.rb +9 -8
- data/lib/webauthn/public_key_credential/request_options.rb +11 -1
- data/lib/webauthn/public_key_credential.rb +24 -5
- data/lib/webauthn/public_key_credential_with_assertion.rb +14 -1
- data/lib/webauthn/relying_party.rb +120 -0
- data/lib/webauthn/u2f_migrator.rb +4 -1
- data/lib/webauthn/version.rb +1 -1
- data/webauthn.gemspec +7 -8
- metadata +40 -59
- data/.travis.yml +0 -39
- data/Appraisals +0 -21
- data/gemfiles/cose_head.gemfile +0 -7
- data/gemfiles/openssl_2_0.gemfile +0 -7
- data/gemfiles/openssl_2_1.gemfile +0 -7
- data/gemfiles/openssl_2_2.gemfile +0 -7
- data/gemfiles/openssl_head.gemfile +0 -7
- data/lib/webauthn/security_utils.rb +0 -20
- data/script/ci/install-openssl +0 -7
- data/script/ci/install-ruby +0 -13
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "webauthn/attestation_statement/base"
|
5
|
+
|
6
|
+
module WebAuthn
|
7
|
+
module AttestationStatement
|
8
|
+
class Apple < Base
|
9
|
+
# Source: https://www.apple.com/certificateauthority/private/
|
10
|
+
ROOT_CERTIFICATE =
|
11
|
+
OpenSSL::X509::Certificate.new(<<~PEM)
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
|
14
|
+
HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
|
15
|
+
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
|
16
|
+
NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
|
17
|
+
A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
|
18
|
+
AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
|
19
|
+
xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
|
20
|
+
pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
|
21
|
+
2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
|
22
|
+
MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
|
23
|
+
jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
|
24
|
+
1bWeT0vT
|
25
|
+
-----END CERTIFICATE-----
|
26
|
+
PEM
|
27
|
+
|
28
|
+
NONCE_EXTENSION_OID = "1.2.840.113635.100.8.2"
|
29
|
+
|
30
|
+
def valid?(authenticator_data, client_data_hash)
|
31
|
+
valid_nonce?(authenticator_data, client_data_hash) &&
|
32
|
+
matching_public_key?(authenticator_data) &&
|
33
|
+
trustworthy? &&
|
34
|
+
[attestation_type, attestation_trust_path]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def valid_nonce?(authenticator_data, client_data_hash)
|
40
|
+
extension = cred_cert&.find_extension(NONCE_EXTENSION_OID)
|
41
|
+
|
42
|
+
if extension
|
43
|
+
sequence = OpenSSL::ASN1.decode(extension.value_der)
|
44
|
+
|
45
|
+
sequence.tag == OpenSSL::ASN1::SEQUENCE &&
|
46
|
+
sequence.value.size == 1 &&
|
47
|
+
sequence.value[0].value[0].value ==
|
48
|
+
OpenSSL::Digest::SHA256.digest(authenticator_data.data + client_data_hash)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def attestation_type
|
53
|
+
WebAuthn::AttestationStatement::ATTESTATION_TYPE_ANONCA
|
54
|
+
end
|
55
|
+
|
56
|
+
def cred_cert
|
57
|
+
attestation_certificate
|
58
|
+
end
|
59
|
+
|
60
|
+
def default_root_certificates
|
61
|
+
[ROOT_CERTIFICATE]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -16,18 +16,21 @@ module WebAuthn
|
|
16
16
|
ATTESTATION_TYPE_SELF = "Self"
|
17
17
|
ATTESTATION_TYPE_ATTCA = "AttCA"
|
18
18
|
ATTESTATION_TYPE_BASIC_OR_ATTCA = "Basic_or_AttCA"
|
19
|
+
ATTESTATION_TYPE_ANONCA = "AnonCA"
|
19
20
|
|
20
21
|
ATTESTATION_TYPES_WITH_ROOT = [
|
21
22
|
ATTESTATION_TYPE_BASIC,
|
22
23
|
ATTESTATION_TYPE_BASIC_OR_ATTCA,
|
23
|
-
ATTESTATION_TYPE_ATTCA
|
24
|
+
ATTESTATION_TYPE_ATTCA,
|
25
|
+
ATTESTATION_TYPE_ANONCA
|
24
26
|
].freeze
|
25
27
|
|
26
28
|
class Base
|
27
29
|
AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4"
|
28
30
|
|
29
|
-
def initialize(statement)
|
31
|
+
def initialize(statement, relying_party = WebAuthn.configuration.relying_party)
|
30
32
|
@statement = statement
|
33
|
+
@relying_party = relying_party
|
31
34
|
end
|
32
35
|
|
33
36
|
def valid?(_authenticator_data, _client_data_hash)
|
@@ -42,32 +45,28 @@ module WebAuthn
|
|
42
45
|
certificates&.first
|
43
46
|
end
|
44
47
|
|
45
|
-
def certificate_chain
|
46
|
-
if certificates
|
47
|
-
certificates[1..-1]
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
48
|
def attestation_certificate_key_id
|
52
|
-
|
49
|
+
attestation_certificate.subject_key_identifier&.unpack("H*")&.[](0)
|
53
50
|
end
|
54
51
|
|
55
52
|
private
|
56
53
|
|
57
|
-
attr_reader :statement
|
54
|
+
attr_reader :statement, :relying_party
|
58
55
|
|
59
56
|
def matching_aaguid?(attested_credential_data_aaguid)
|
60
|
-
extension = attestation_certificate&.
|
57
|
+
extension = attestation_certificate&.find_extension(AAGUID_EXTENSION_OID)
|
61
58
|
if extension
|
62
|
-
|
63
|
-
|
64
|
-
extension.to_der[-WebAuthn::AuthenticatorData::AttestedCredentialData::AAGUID_LENGTH..-1] ==
|
65
|
-
attested_credential_data_aaguid
|
59
|
+
aaguid_value = OpenSSL::ASN1.decode(extension.value_der).value
|
60
|
+
aaguid_value == attested_credential_data_aaguid
|
66
61
|
else
|
67
62
|
true
|
68
63
|
end
|
69
64
|
end
|
70
65
|
|
66
|
+
def matching_public_key?(authenticator_data)
|
67
|
+
attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der
|
68
|
+
end
|
69
|
+
|
71
70
|
def certificates
|
72
71
|
@certificates ||=
|
73
72
|
raw_certificates&.map do |raw_certificate|
|
@@ -95,10 +94,10 @@ module WebAuthn
|
|
95
94
|
|
96
95
|
def trustworthy?(aaguid: nil, attestation_certificate_key_id: nil)
|
97
96
|
if ATTESTATION_TYPES_WITH_ROOT.include?(attestation_type)
|
98
|
-
|
97
|
+
relying_party.acceptable_attestation_types.include?(attestation_type) &&
|
99
98
|
valid_certificate_chain?(aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id)
|
100
99
|
else
|
101
|
-
|
100
|
+
relying_party.acceptable_attestation_types.include?(attestation_type)
|
102
101
|
end
|
103
102
|
end
|
104
103
|
|
@@ -122,7 +121,7 @@ module WebAuthn
|
|
122
121
|
|
123
122
|
def root_certificates(aaguid: nil, attestation_certificate_key_id: nil)
|
124
123
|
root_certificates =
|
125
|
-
|
124
|
+
relying_party.attestation_root_certificates_finders.reduce([]) do |certs, finder|
|
126
125
|
if certs.empty?
|
127
126
|
finder.find(
|
128
127
|
attestation_format: format,
|
@@ -141,15 +140,6 @@ module WebAuthn
|
|
141
140
|
end
|
142
141
|
end
|
143
142
|
|
144
|
-
def raw_subject_key_identifier
|
145
|
-
extension = attestation_certificate.extensions.detect { |ext| ext.oid == "subjectKeyIdentifier" }
|
146
|
-
return unless extension
|
147
|
-
|
148
|
-
ext_asn1 = OpenSSL::ASN1.decode(extension.to_der)
|
149
|
-
ext_value = ext_asn1.value.last
|
150
|
-
OpenSSL::ASN1.decode(ext_value.value).value
|
151
|
-
end
|
152
|
-
|
153
143
|
def valid_signature?(authenticator_data, client_data_hash, public_key = attestation_certificate.public_key)
|
154
144
|
raise("Incompatible algorithm and key") unless cose_algorithm.compatible_key?(public_key)
|
155
145
|
|
@@ -169,14 +159,10 @@ module WebAuthn
|
|
169
159
|
def cose_algorithm
|
170
160
|
@cose_algorithm ||=
|
171
161
|
COSE::Algorithm.find(algorithm).tap do |alg|
|
172
|
-
alg &&
|
162
|
+
alg && relying_party.algorithms.include?(alg.name) ||
|
173
163
|
raise(UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}")
|
174
164
|
end
|
175
165
|
end
|
176
|
-
|
177
|
-
def configuration
|
178
|
-
WebAuthn.configuration
|
179
|
-
end
|
180
166
|
end
|
181
167
|
end
|
182
168
|
end
|
@@ -6,12 +6,18 @@ module WebAuthn
|
|
6
6
|
module AttestationStatement
|
7
7
|
class None < Base
|
8
8
|
def valid?(*_args)
|
9
|
-
if statement == {}
|
9
|
+
if statement == {} && trustworthy?
|
10
10
|
[WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE, nil]
|
11
11
|
else
|
12
12
|
false
|
13
13
|
end
|
14
14
|
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def attestation_type
|
19
|
+
WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE
|
20
|
+
end
|
15
21
|
end
|
16
22
|
end
|
17
23
|
end
|
@@ -46,7 +46,7 @@ module WebAuthn
|
|
46
46
|
|
47
47
|
attestation_certificate.version == 2 &&
|
48
48
|
subject.assoc('OU')&.at(1) == "Authenticator Attestation" &&
|
49
|
-
attestation_certificate.
|
49
|
+
attestation_certificate.find_extension('basicConstraints')&.value == 'CA:FALSE'
|
50
50
|
else
|
51
51
|
true
|
52
52
|
end
|
@@ -42,7 +42,7 @@ module WebAuthn
|
|
42
42
|
OpenSSL::Digest.digest(cose_algorithm.hash_function, certified_extra_data),
|
43
43
|
signature_algorithm: tpm_algorithm[:signature],
|
44
44
|
hash_algorithm: tpm_algorithm[:hash],
|
45
|
-
|
45
|
+
trusted_certificates: root_certificates(aaguid: aaguid)
|
46
46
|
)
|
47
47
|
|
48
48
|
key_attestation.valid? && key_attestation.key && key_attestation.key.to_pem == key.to_pem
|
@@ -54,7 +54,7 @@ module WebAuthn
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def default_root_certificates
|
57
|
-
::TPM::KeyAttestation::
|
57
|
+
::TPM::KeyAttestation::TRUSTED_CERTIFICATES
|
58
58
|
end
|
59
59
|
|
60
60
|
def tpm_algorithm
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "webauthn/attestation_statement/android_key"
|
4
4
|
require "webauthn/attestation_statement/android_safetynet"
|
5
|
+
require "webauthn/attestation_statement/apple"
|
5
6
|
require "webauthn/attestation_statement/fido_u2f"
|
6
7
|
require "webauthn/attestation_statement/none"
|
7
8
|
require "webauthn/attestation_statement/packed"
|
@@ -18,6 +19,7 @@ module WebAuthn
|
|
18
19
|
ATTESTATION_FORMAT_ANDROID_SAFETYNET = "android-safetynet"
|
19
20
|
ATTESTATION_FORMAT_ANDROID_KEY = "android-key"
|
20
21
|
ATTESTATION_FORMAT_TPM = "tpm"
|
22
|
+
ATTESTATION_FORMAT_APPLE = "apple"
|
21
23
|
|
22
24
|
FORMAT_TO_CLASS = {
|
23
25
|
ATTESTATION_FORMAT_NONE => WebAuthn::AttestationStatement::None,
|
@@ -25,14 +27,15 @@ module WebAuthn
|
|
25
27
|
ATTESTATION_FORMAT_PACKED => WebAuthn::AttestationStatement::Packed,
|
26
28
|
ATTESTATION_FORMAT_ANDROID_SAFETYNET => WebAuthn::AttestationStatement::AndroidSafetynet,
|
27
29
|
ATTESTATION_FORMAT_ANDROID_KEY => WebAuthn::AttestationStatement::AndroidKey,
|
28
|
-
ATTESTATION_FORMAT_TPM => WebAuthn::AttestationStatement::TPM
|
30
|
+
ATTESTATION_FORMAT_TPM => WebAuthn::AttestationStatement::TPM,
|
31
|
+
ATTESTATION_FORMAT_APPLE => WebAuthn::AttestationStatement::Apple
|
29
32
|
}.freeze
|
30
33
|
|
31
|
-
def self.from(format, statement)
|
34
|
+
def self.from(format, statement, relying_party: WebAuthn.configuration.relying_party)
|
32
35
|
klass = FORMAT_TO_CLASS[format]
|
33
36
|
|
34
37
|
if klass
|
35
|
-
klass.new(statement)
|
38
|
+
klass.new(statement, relying_party)
|
36
39
|
else
|
37
40
|
raise(FormatNotSupportedError, "Unsupported attestation format '#{format}'")
|
38
41
|
end
|
@@ -10,8 +10,8 @@ module WebAuthn
|
|
10
10
|
class SignCountVerificationError < VerificationError; end
|
11
11
|
|
12
12
|
class AuthenticatorAssertionResponse < AuthenticatorResponse
|
13
|
-
def self.from_client(response)
|
14
|
-
encoder =
|
13
|
+
def self.from_client(response, relying_party: WebAuthn.configuration.relying_party)
|
14
|
+
encoder = relying_party.encoder
|
15
15
|
|
16
16
|
user_handle =
|
17
17
|
if response["userHandle"]
|
@@ -22,7 +22,8 @@ module WebAuthn
|
|
22
22
|
authenticator_data: encoder.decode(response["authenticatorData"]),
|
23
23
|
client_data_json: encoder.decode(response["clientDataJSON"]),
|
24
24
|
signature: encoder.decode(response["signature"]),
|
25
|
-
user_handle: user_handle
|
25
|
+
user_handle: user_handle,
|
26
|
+
relying_party: relying_party
|
26
27
|
)
|
27
28
|
end
|
28
29
|
|
@@ -18,12 +18,13 @@ module WebAuthn
|
|
18
18
|
class AuthenticatorAttestationResponse < AuthenticatorResponse
|
19
19
|
extend Forwardable
|
20
20
|
|
21
|
-
def self.from_client(response)
|
22
|
-
encoder =
|
21
|
+
def self.from_client(response, relying_party: WebAuthn.configuration.relying_party)
|
22
|
+
encoder = relying_party.encoder
|
23
23
|
|
24
24
|
new(
|
25
25
|
attestation_object: encoder.decode(response["attestationObject"]),
|
26
|
-
client_data_json: encoder.decode(response["clientDataJSON"])
|
26
|
+
client_data_json: encoder.decode(response["clientDataJSON"]),
|
27
|
+
relying_party: relying_party
|
27
28
|
)
|
28
29
|
end
|
29
30
|
|
@@ -33,13 +34,14 @@ module WebAuthn
|
|
33
34
|
super(**options)
|
34
35
|
|
35
36
|
@attestation_object_bytes = attestation_object
|
37
|
+
@relying_party = relying_party
|
36
38
|
end
|
37
39
|
|
38
40
|
def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil)
|
39
41
|
super
|
40
42
|
|
41
43
|
verify_item(:attested_credential)
|
42
|
-
if
|
44
|
+
if relying_party.verify_attestation_statement
|
43
45
|
verify_item(:attestation_statement)
|
44
46
|
end
|
45
47
|
|
@@ -47,7 +49,7 @@ module WebAuthn
|
|
47
49
|
end
|
48
50
|
|
49
51
|
def attestation_object
|
50
|
-
@attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes)
|
52
|
+
@attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes, relying_party)
|
51
53
|
end
|
52
54
|
|
53
55
|
def_delegators(
|
@@ -63,14 +65,15 @@ module WebAuthn
|
|
63
65
|
|
64
66
|
private
|
65
67
|
|
66
|
-
attr_reader :attestation_object_bytes
|
68
|
+
attr_reader :attestation_object_bytes, :relying_party
|
67
69
|
|
68
70
|
def type
|
69
71
|
WebAuthn::TYPES[:create]
|
70
72
|
end
|
71
73
|
|
72
74
|
def valid_attested_credential?
|
73
|
-
attestation_object.valid_attested_credential?
|
75
|
+
attestation_object.valid_attested_credential? &&
|
76
|
+
relying_party.algorithms.include?(authenticator_data.credential.algorithm)
|
74
77
|
end
|
75
78
|
|
76
79
|
def valid_attestation_statement?
|
@@ -22,9 +22,8 @@ module WebAuthn
|
|
22
22
|
count_bytes_remaining :trailing_bytes_length
|
23
23
|
string :trailing_bytes, length: :trailing_bytes_length
|
24
24
|
|
25
|
-
# TODO: use keyword_init when we dropped Ruby 2.4 support
|
26
25
|
Credential =
|
27
|
-
Struct.new(:id, :public_key) do
|
26
|
+
Struct.new(:id, :public_key, :algorithm, keyword_init: true) do
|
28
27
|
def public_key_object
|
29
28
|
COSE::Key.deserialize(public_key).to_pkey
|
30
29
|
end
|
@@ -47,7 +46,7 @@ module WebAuthn
|
|
47
46
|
def credential
|
48
47
|
@credential ||=
|
49
48
|
if valid?
|
50
|
-
Credential.new(id, public_key)
|
49
|
+
Credential.new(id: id, public_key: public_key, algorithm: algorithm)
|
51
50
|
end
|
52
51
|
end
|
53
52
|
|
@@ -59,10 +58,16 @@ module WebAuthn
|
|
59
58
|
|
60
59
|
private
|
61
60
|
|
61
|
+
def algorithm
|
62
|
+
COSE::Algorithm.find(cose_key.alg).name
|
63
|
+
end
|
64
|
+
|
62
65
|
def valid_credential_public_key?
|
63
|
-
cose_key
|
66
|
+
!!cose_key.alg
|
67
|
+
end
|
64
68
|
|
65
|
-
|
69
|
+
def cose_key
|
70
|
+
@cose_key ||= COSE::Key.deserialize(public_key)
|
66
71
|
end
|
67
72
|
|
68
73
|
def public_key
|
@@ -19,9 +19,9 @@ module WebAuthn
|
|
19
19
|
struct :flags do
|
20
20
|
bit1 :extension_data_included
|
21
21
|
bit1 :attested_credential_data_included
|
22
|
-
bit1 :reserved_for_future_use_4
|
23
|
-
bit1 :reserved_for_future_use_3
|
24
22
|
bit1 :reserved_for_future_use_2
|
23
|
+
bit1 :backup_state
|
24
|
+
bit1 :backup_eligibility
|
25
25
|
bit1 :user_verified
|
26
26
|
bit1 :reserved_for_future_use_1
|
27
27
|
bit1 :user_present
|
@@ -58,6 +58,14 @@ module WebAuthn
|
|
58
58
|
flags.user_verified == 1
|
59
59
|
end
|
60
60
|
|
61
|
+
def credential_backup_eligible?
|
62
|
+
flags.backup_eligibility == 1
|
63
|
+
end
|
64
|
+
|
65
|
+
def credential_backed_up?
|
66
|
+
flags.backup_state == 1
|
67
|
+
end
|
68
|
+
|
61
69
|
def attested_credential_data_included?
|
62
70
|
flags.attested_credential_data_included == 1
|
63
71
|
end
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require "webauthn/authenticator_data"
|
4
4
|
require "webauthn/client_data"
|
5
5
|
require "webauthn/error"
|
6
|
-
require "webauthn/security_utils"
|
7
6
|
|
8
7
|
module WebAuthn
|
9
8
|
TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze
|
@@ -20,13 +19,14 @@ module WebAuthn
|
|
20
19
|
class UserVerifiedVerificationError < VerificationError; end
|
21
20
|
|
22
21
|
class AuthenticatorResponse
|
23
|
-
def initialize(client_data_json:)
|
22
|
+
def initialize(client_data_json:, relying_party: WebAuthn.configuration.relying_party)
|
24
23
|
@client_data_json = client_data_json
|
24
|
+
@relying_party = relying_party
|
25
25
|
end
|
26
26
|
|
27
27
|
def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil)
|
28
|
-
expected_origin ||=
|
29
|
-
rp_id ||=
|
28
|
+
expected_origin ||= relying_party.origin || raise("Unspecified expected origin")
|
29
|
+
rp_id ||= relying_party.id
|
30
30
|
|
31
31
|
verify_item(:type)
|
32
32
|
verify_item(:token_binding)
|
@@ -35,7 +35,7 @@ module WebAuthn
|
|
35
35
|
verify_item(:authenticator_data)
|
36
36
|
verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin))
|
37
37
|
|
38
|
-
if !
|
38
|
+
if !relying_party.silent_authentication
|
39
39
|
verify_item(:user_presence)
|
40
40
|
end
|
41
41
|
|
@@ -58,7 +58,7 @@ module WebAuthn
|
|
58
58
|
|
59
59
|
private
|
60
60
|
|
61
|
-
attr_reader :client_data_json
|
61
|
+
attr_reader :client_data_json, :relying_party
|
62
62
|
|
63
63
|
def verify_item(item, *args)
|
64
64
|
if send("valid_#{item}?", *args)
|
@@ -79,7 +79,7 @@ module WebAuthn
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def valid_challenge?(expected_challenge)
|
82
|
-
|
82
|
+
OpenSSL.secure_compare(client_data.challenge, expected_challenge)
|
83
83
|
end
|
84
84
|
|
85
85
|
def valid_origin?(expected_origin)
|
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require "webauthn/error"
|
3
|
+
require 'forwardable'
|
4
|
+
require 'webauthn/relying_party'
|
6
5
|
|
7
6
|
module WebAuthn
|
8
7
|
def self.configuration
|
@@ -13,54 +12,51 @@ module WebAuthn
|
|
13
12
|
yield(configuration)
|
14
13
|
end
|
15
14
|
|
16
|
-
class RootCertificateFinderNotSupportedError < Error; end
|
17
|
-
|
18
15
|
class Configuration
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
16
|
+
extend Forwardable
|
17
|
+
|
18
|
+
def_delegators :@relying_party,
|
19
|
+
:algorithms,
|
20
|
+
:algorithms=,
|
21
|
+
:encoding,
|
22
|
+
:encoding=,
|
23
|
+
:origin,
|
24
|
+
:origin=,
|
25
|
+
:verify_attestation_statement,
|
26
|
+
:verify_attestation_statement=,
|
27
|
+
:credential_options_timeout,
|
28
|
+
:credential_options_timeout=,
|
29
|
+
:silent_authentication,
|
30
|
+
:silent_authentication=,
|
31
|
+
:acceptable_attestation_types,
|
32
|
+
:acceptable_attestation_types=,
|
33
|
+
:attestation_root_certificates_finders,
|
34
|
+
:attestation_root_certificates_finders=,
|
35
|
+
:encoder,
|
36
|
+
:encoder=,
|
37
|
+
:legacy_u2f_appid,
|
38
|
+
:legacy_u2f_appid=
|
39
|
+
|
40
|
+
attr_reader :relying_party
|
35
41
|
|
36
42
|
def initialize
|
37
|
-
@
|
38
|
-
@encoding = WebAuthn::Encoder::STANDARD_ENCODING
|
39
|
-
@verify_attestation_statement = true
|
40
|
-
@credential_options_timeout = 120000
|
41
|
-
@silent_authentication = false
|
42
|
-
@acceptable_attestation_types = ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA']
|
43
|
-
@attestation_root_certificates_finders = []
|
43
|
+
@relying_party = RelyingParty.new
|
44
44
|
end
|
45
45
|
|
46
|
-
|
47
|
-
|
48
|
-
def encoder
|
49
|
-
@encoder ||= WebAuthn::Encoder.new(encoding)
|
46
|
+
def rp_name
|
47
|
+
relying_party.name
|
50
48
|
end
|
51
49
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
end
|
50
|
+
def rp_name=(name)
|
51
|
+
relying_party.name = name
|
52
|
+
end
|
56
53
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
61
|
-
end
|
54
|
+
def rp_id
|
55
|
+
relying_party.id
|
56
|
+
end
|
62
57
|
|
63
|
-
|
58
|
+
def rp_id=(id)
|
59
|
+
relying_party.id = id
|
64
60
|
end
|
65
61
|
end
|
66
62
|
end
|
data/lib/webauthn/credential.rb
CHANGED
@@ -4,6 +4,7 @@ require "webauthn/public_key_credential/creation_options"
|
|
4
4
|
require "webauthn/public_key_credential/request_options"
|
5
5
|
require "webauthn/public_key_credential_with_assertion"
|
6
6
|
require "webauthn/public_key_credential_with_attestation"
|
7
|
+
require "webauthn/relying_party"
|
7
8
|
|
8
9
|
module WebAuthn
|
9
10
|
module Credential
|
@@ -15,12 +16,12 @@ module WebAuthn
|
|
15
16
|
WebAuthn::PublicKeyCredential::RequestOptions.new(**keyword_arguments)
|
16
17
|
end
|
17
18
|
|
18
|
-
def self.from_create(credential)
|
19
|
-
WebAuthn::PublicKeyCredentialWithAttestation.from_client(credential)
|
19
|
+
def self.from_create(credential, relying_party: WebAuthn.configuration.relying_party)
|
20
|
+
WebAuthn::PublicKeyCredentialWithAttestation.from_client(credential, relying_party: relying_party)
|
20
21
|
end
|
21
22
|
|
22
|
-
def self.from_get(credential)
|
23
|
-
WebAuthn::PublicKeyCredentialWithAssertion.from_client(credential)
|
23
|
+
def self.from_get(credential, relying_party: WebAuthn.configuration.relying_party)
|
24
|
+
WebAuthn::PublicKeyCredentialWithAssertion.from_client(credential, relying_party: relying_party)
|
24
25
|
end
|
25
26
|
end
|
26
27
|
end
|
@@ -16,6 +16,8 @@ module WebAuthn
|
|
16
16
|
attr_accessor :allow_credentials, :extensions, :user_verification
|
17
17
|
|
18
18
|
def initialize(allow_credentials: [], extensions: nil, user_verification: nil)
|
19
|
+
super()
|
20
|
+
|
19
21
|
@allow_credentials = allow_credentials
|
20
22
|
@extensions = extensions
|
21
23
|
@user_verification = user_verification
|
@@ -13,6 +13,8 @@ module WebAuthn
|
|
13
13
|
credential_key:,
|
14
14
|
user_present: true,
|
15
15
|
user_verified: false,
|
16
|
+
backup_eligibility: false,
|
17
|
+
backup_state: false,
|
16
18
|
attested_credential_data: true,
|
17
19
|
sign_count: 0,
|
18
20
|
extensions: nil
|
@@ -23,6 +25,8 @@ module WebAuthn
|
|
23
25
|
@credential_key = credential_key
|
24
26
|
@user_present = user_present
|
25
27
|
@user_verified = user_verified
|
28
|
+
@backup_eligibility = backup_eligibility
|
29
|
+
@backup_state = backup_state
|
26
30
|
@attested_credential_data = attested_credential_data
|
27
31
|
@sign_count = sign_count
|
28
32
|
@extensions = extensions
|
@@ -45,6 +49,8 @@ module WebAuthn
|
|
45
49
|
:credential_key,
|
46
50
|
:user_present,
|
47
51
|
:user_verified,
|
52
|
+
:backup_eligibility,
|
53
|
+
:backup_state,
|
48
54
|
:attested_credential_data,
|
49
55
|
:sign_count,
|
50
56
|
:extensions
|
@@ -63,6 +69,8 @@ module WebAuthn
|
|
63
69
|
credential: credential_data,
|
64
70
|
user_present: user_present,
|
65
71
|
user_verified: user_verified,
|
72
|
+
backup_eligibility: backup_eligibility,
|
73
|
+
backup_state: backup_state,
|
66
74
|
sign_count: 0,
|
67
75
|
extensions: extensions
|
68
76
|
)
|