webauthn 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.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
|
)
|