webauthn 2.2.0 → 2.5.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 +36 -0
- data/.rubocop.yml +60 -0
- data/Appraisals +2 -10
- data/CHANGELOG.md +53 -0
- data/README.md +71 -9
- data/SECURITY.md +6 -3
- data/gemfiles/{openssl_2_0.gemfile → openssl_2_2.gemfile} +1 -1
- data/lib/cose/rsapkcs1_algorithm.rb +11 -0
- data/lib/webauthn/attestation_object.rb +2 -2
- data/lib/webauthn/attestation_statement.rb +4 -1
- data/lib/webauthn/attestation_statement/android_key.rb +0 -11
- 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 +36 -14
- data/lib/webauthn/attestation_statement/fido_u2f.rb +2 -5
- data/lib/webauthn/attestation_statement/none.rb +7 -1
- data/lib/webauthn/attestation_statement/packed.rb +10 -23
- data/lib/webauthn/attestation_statement/tpm.rb +10 -20
- data/lib/webauthn/authenticator_assertion_response.rb +1 -4
- data/lib/webauthn/authenticator_attestation_response.rb +2 -2
- data/lib/webauthn/configuration.rb +2 -6
- data/lib/webauthn/credential_creation_options.rb +2 -0
- data/lib/webauthn/credential_request_options.rb +2 -0
- data/lib/webauthn/fake_authenticator.rb +16 -4
- data/lib/webauthn/fake_authenticator/attestation_object.rb +7 -3
- data/lib/webauthn/fake_client.rb +21 -4
- data/lib/webauthn/public_key.rb +21 -2
- data/lib/webauthn/public_key_credential.rb +13 -3
- data/lib/webauthn/public_key_credential/entity.rb +3 -4
- data/lib/webauthn/version.rb +1 -1
- data/webauthn.gemspec +7 -6
- metadata +34 -22
- data/.travis.yml +0 -26
- data/gemfiles/cose_head.gemfile +0 -7
- data/gemfiles/openssl_head.gemfile +0 -7
- data/lib/webauthn/signature_verifier.rb +0 -52
@@ -3,7 +3,6 @@
|
|
3
3
|
require "android_key_attestation"
|
4
4
|
require "openssl"
|
5
5
|
require "webauthn/attestation_statement/base"
|
6
|
-
require "webauthn/signature_verifier"
|
7
6
|
|
8
7
|
module WebAuthn
|
9
8
|
module AttestationStatement
|
@@ -21,16 +20,6 @@ module WebAuthn
|
|
21
20
|
|
22
21
|
private
|
23
22
|
|
24
|
-
def valid_signature?(authenticator_data, client_data_hash)
|
25
|
-
WebAuthn::SignatureVerifier
|
26
|
-
.new(algorithm, attestation_certificate.public_key)
|
27
|
-
.verify(signature, authenticator_data.data + client_data_hash)
|
28
|
-
end
|
29
|
-
|
30
|
-
def matching_public_key?(authenticator_data)
|
31
|
-
attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der
|
32
|
-
end
|
33
|
-
|
34
23
|
def valid_attestation_challenge?(client_data_hash)
|
35
24
|
android_key_attestation.verify_challenge(client_data_hash)
|
36
25
|
rescue AndroidKeyAttestation::ChallengeMismatchError
|
@@ -16,10 +16,6 @@ module WebAuthn
|
|
16
16
|
[attestation_type, attestation_trust_path]
|
17
17
|
end
|
18
18
|
|
19
|
-
def attestation_certificate
|
20
|
-
attestation_trust_path.first
|
21
|
-
end
|
22
|
-
|
23
19
|
private
|
24
20
|
|
25
21
|
def valid_response?(authenticator_data, client_data_hash)
|
@@ -52,7 +48,7 @@ module WebAuthn
|
|
52
48
|
end
|
53
49
|
|
54
50
|
# SafetyNetAttestation returns full chain including root, WebAuthn expects only the x5c certificates
|
55
|
-
def
|
51
|
+
def certificates
|
56
52
|
attestation_response.certificate_chain[0..-2]
|
57
53
|
end
|
58
54
|
|
@@ -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&.extensions&.detect { |ext| ext.oid == NONCE_EXTENSION_OID }
|
41
|
+
|
42
|
+
if extension
|
43
|
+
sequence = OpenSSL::ASN1.decode(OpenSSL::ASN1.decode(extension.to_der).value[1].value)
|
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
|
@@ -1,27 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "cose/algorithm"
|
4
|
+
require "cose/error"
|
5
|
+
require "cose/rsapkcs1_algorithm"
|
3
6
|
require "openssl"
|
4
7
|
require "webauthn/authenticator_data/attested_credential_data"
|
5
8
|
require "webauthn/error"
|
6
9
|
|
7
10
|
module WebAuthn
|
8
11
|
module AttestationStatement
|
12
|
+
class UnsupportedAlgorithm < Error; end
|
13
|
+
|
9
14
|
ATTESTATION_TYPE_NONE = "None"
|
10
15
|
ATTESTATION_TYPE_BASIC = "Basic"
|
11
16
|
ATTESTATION_TYPE_SELF = "Self"
|
12
17
|
ATTESTATION_TYPE_ATTCA = "AttCA"
|
13
|
-
ATTESTATION_TYPE_ECDAA = "ECDAA"
|
14
18
|
ATTESTATION_TYPE_BASIC_OR_ATTCA = "Basic_or_AttCA"
|
19
|
+
ATTESTATION_TYPE_ANONCA = "AnonCA"
|
15
20
|
|
16
21
|
ATTESTATION_TYPES_WITH_ROOT = [
|
17
22
|
ATTESTATION_TYPE_BASIC,
|
18
23
|
ATTESTATION_TYPE_BASIC_OR_ATTCA,
|
19
|
-
ATTESTATION_TYPE_ATTCA
|
24
|
+
ATTESTATION_TYPE_ATTCA,
|
25
|
+
ATTESTATION_TYPE_ANONCA
|
20
26
|
].freeze
|
21
27
|
|
22
28
|
class Base
|
23
|
-
class NotSupportedError < Error; end
|
24
|
-
|
25
29
|
AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4"
|
26
30
|
|
27
31
|
def initialize(statement)
|
@@ -40,12 +44,6 @@ module WebAuthn
|
|
40
44
|
certificates&.first
|
41
45
|
end
|
42
46
|
|
43
|
-
def certificate_chain
|
44
|
-
if certificates
|
45
|
-
certificates[1..-1]
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
47
|
def attestation_certificate_key_id
|
50
48
|
raw_subject_key_identifier&.unpack("H*")&.[](0)
|
51
49
|
end
|
@@ -66,6 +64,10 @@ module WebAuthn
|
|
66
64
|
end
|
67
65
|
end
|
68
66
|
|
67
|
+
def matching_public_key?(authenticator_data)
|
68
|
+
attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der
|
69
|
+
end
|
70
|
+
|
69
71
|
def certificates
|
70
72
|
@certificates ||=
|
71
73
|
raw_certificates&.map do |raw_certificate|
|
@@ -81,10 +83,6 @@ module WebAuthn
|
|
81
83
|
statement["x5c"]
|
82
84
|
end
|
83
85
|
|
84
|
-
def raw_ecdaa_key_id
|
85
|
-
statement["ecdaaKeyId"]
|
86
|
-
end
|
87
|
-
|
88
86
|
def signature
|
89
87
|
statement["sig"]
|
90
88
|
end
|
@@ -152,6 +150,30 @@ module WebAuthn
|
|
152
150
|
OpenSSL::ASN1.decode(ext_value.value).value
|
153
151
|
end
|
154
152
|
|
153
|
+
def valid_signature?(authenticator_data, client_data_hash, public_key = attestation_certificate.public_key)
|
154
|
+
raise("Incompatible algorithm and key") unless cose_algorithm.compatible_key?(public_key)
|
155
|
+
|
156
|
+
cose_algorithm.verify(
|
157
|
+
public_key,
|
158
|
+
signature,
|
159
|
+
verification_data(authenticator_data, client_data_hash)
|
160
|
+
)
|
161
|
+
rescue COSE::Error
|
162
|
+
false
|
163
|
+
end
|
164
|
+
|
165
|
+
def verification_data(authenticator_data, client_data_hash)
|
166
|
+
authenticator_data.data + client_data_hash
|
167
|
+
end
|
168
|
+
|
169
|
+
def cose_algorithm
|
170
|
+
@cose_algorithm ||=
|
171
|
+
COSE::Algorithm.find(algorithm).tap do |alg|
|
172
|
+
alg && configuration.algorithms.include?(alg.name) ||
|
173
|
+
raise(UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
155
177
|
def configuration
|
156
178
|
WebAuthn.configuration
|
157
179
|
end
|
@@ -4,7 +4,6 @@ require "cose"
|
|
4
4
|
require "openssl"
|
5
5
|
require "webauthn/attestation_statement/base"
|
6
6
|
require "webauthn/attestation_statement/fido_u2f/public_key"
|
7
|
-
require "webauthn/signature_verifier"
|
8
7
|
|
9
8
|
module WebAuthn
|
10
9
|
module AttestationStatement
|
@@ -48,10 +47,8 @@ module WebAuthn
|
|
48
47
|
attested_credential_data_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID
|
49
48
|
end
|
50
49
|
|
51
|
-
def
|
52
|
-
|
53
|
-
.new(VALID_ATTESTATION_CERTIFICATE_ALGORITHM, certificate_public_key)
|
54
|
-
.verify(signature, verification_data(authenticator_data, client_data_hash))
|
50
|
+
def algorithm
|
51
|
+
VALID_ATTESTATION_CERTIFICATE_ALGORITHM.id
|
55
52
|
end
|
56
53
|
|
57
54
|
def verification_data(authenticator_data, client_data_hash)
|
@@ -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
|
@@ -2,17 +2,13 @@
|
|
2
2
|
|
3
3
|
require "openssl"
|
4
4
|
require "webauthn/attestation_statement/base"
|
5
|
-
require "webauthn/signature_verifier"
|
6
5
|
|
7
6
|
module WebAuthn
|
8
7
|
# Implements https://www.w3.org/TR/2018/CR-webauthn-20180807/#packed-attestation
|
9
|
-
# ECDAA attestation is unsupported.
|
10
8
|
module AttestationStatement
|
11
9
|
class Packed < Base
|
12
10
|
# Follows "Verification procedure"
|
13
11
|
def valid?(authenticator_data, client_data_hash)
|
14
|
-
check_unsupported_feature
|
15
|
-
|
16
12
|
valid_format? &&
|
17
13
|
valid_algorithm?(authenticator_data.credential) &&
|
18
14
|
valid_ec_public_keys?(authenticator_data.credential) &&
|
@@ -30,19 +26,11 @@ module WebAuthn
|
|
30
26
|
end
|
31
27
|
|
32
28
|
def self_attestation?
|
33
|
-
!raw_certificates
|
29
|
+
!raw_certificates
|
34
30
|
end
|
35
31
|
|
36
32
|
def valid_format?
|
37
|
-
algorithm && signature
|
38
|
-
[raw_certificates, raw_ecdaa_key_id].compact.size < 2
|
39
|
-
)
|
40
|
-
end
|
41
|
-
|
42
|
-
def check_unsupported_feature
|
43
|
-
if raw_ecdaa_key_id
|
44
|
-
raise NotSupportedError, "ecdaaKeyId of the packed attestation format is not implemented yet"
|
45
|
-
end
|
33
|
+
algorithm && signature
|
46
34
|
end
|
47
35
|
|
48
36
|
def valid_ec_public_keys?(credential)
|
@@ -64,15 +52,6 @@ module WebAuthn
|
|
64
52
|
end
|
65
53
|
end
|
66
54
|
|
67
|
-
def valid_signature?(authenticator_data, client_data_hash)
|
68
|
-
signature_verifier = WebAuthn::SignatureVerifier.new(
|
69
|
-
algorithm,
|
70
|
-
attestation_certificate&.public_key || authenticator_data.credential.public_key_object
|
71
|
-
)
|
72
|
-
|
73
|
-
signature_verifier.verify(signature, authenticator_data.data + client_data_hash)
|
74
|
-
end
|
75
|
-
|
76
55
|
def attestation_type
|
77
56
|
if attestation_trust_path
|
78
57
|
WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA # FIXME: use metadata if available
|
@@ -80,6 +59,14 @@ module WebAuthn
|
|
80
59
|
WebAuthn::AttestationStatement::ATTESTATION_TYPE_SELF
|
81
60
|
end
|
82
61
|
end
|
62
|
+
|
63
|
+
def valid_signature?(authenticator_data, client_data_hash)
|
64
|
+
super(
|
65
|
+
authenticator_data,
|
66
|
+
client_data_hash,
|
67
|
+
attestation_certificate&.public_key || authenticator_data.credential.public_key_object
|
68
|
+
)
|
69
|
+
end
|
83
70
|
end
|
84
71
|
end
|
85
72
|
end
|
@@ -4,7 +4,6 @@ require "cose/algorithm"
|
|
4
4
|
require "openssl"
|
5
5
|
require "tpm/key_attestation"
|
6
6
|
require "webauthn/attestation_statement/base"
|
7
|
-
require "webauthn/signature_verifier"
|
8
7
|
|
9
8
|
module WebAuthn
|
10
9
|
module AttestationStatement
|
@@ -19,23 +18,16 @@ module WebAuthn
|
|
19
18
|
}.freeze
|
20
19
|
|
21
20
|
def valid?(authenticator_data, client_data_hash)
|
22
|
-
|
23
|
-
when ATTESTATION_TYPE_ATTCA
|
21
|
+
attestation_type == ATTESTATION_TYPE_ATTCA &&
|
24
22
|
ver == TPM_V2 &&
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
when ATTESTATION_TYPE_ECDAA
|
34
|
-
raise(
|
35
|
-
WebAuthn::AttestationStatement::Base::NotSupportedError,
|
36
|
-
"Attestation type ECDAA is not supported"
|
37
|
-
)
|
38
|
-
end
|
23
|
+
valid_key_attestation?(
|
24
|
+
authenticator_data.data + client_data_hash,
|
25
|
+
authenticator_data.credential.public_key_object,
|
26
|
+
authenticator_data.aaguid
|
27
|
+
) &&
|
28
|
+
matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
|
29
|
+
trustworthy?(aaguid: authenticator_data.aaguid) &&
|
30
|
+
[attestation_type, attestation_trust_path]
|
39
31
|
end
|
40
32
|
|
41
33
|
private
|
@@ -78,10 +70,8 @@ module WebAuthn
|
|
78
70
|
end
|
79
71
|
|
80
72
|
def attestation_type
|
81
|
-
if raw_certificates
|
73
|
+
if raw_certificates
|
82
74
|
ATTESTATION_TYPE_ATTCA
|
83
|
-
elsif raw_ecdaa_key_id && !raw_certificates
|
84
|
-
ATTESTATION_TYPE_ECDAA
|
85
75
|
else
|
86
76
|
raise "Attestation type invalid"
|
87
77
|
end
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require "webauthn/authenticator_data"
|
4
4
|
require "webauthn/authenticator_response"
|
5
5
|
require "webauthn/encoder"
|
6
|
-
require "webauthn/signature_verifier"
|
7
6
|
require "webauthn/public_key"
|
8
7
|
|
9
8
|
module WebAuthn
|
@@ -54,9 +53,7 @@ module WebAuthn
|
|
54
53
|
attr_reader :authenticator_data_bytes, :signature
|
55
54
|
|
56
55
|
def valid_signature?(webauthn_public_key)
|
57
|
-
|
58
|
-
.new(webauthn_public_key.alg, webauthn_public_key.pkey)
|
59
|
-
.verify(signature, authenticator_data_bytes + client_data.hash)
|
56
|
+
webauthn_public_key.verify(signature, authenticator_data_bytes + client_data.hash)
|
60
57
|
end
|
61
58
|
|
62
59
|
def valid_sign_count?(stored_sign_count)
|
@@ -16,6 +16,8 @@ module WebAuthn
|
|
16
16
|
class AttestedCredentialVerificationError < VerificationError; end
|
17
17
|
|
18
18
|
class AuthenticatorAttestationResponse < AuthenticatorResponse
|
19
|
+
extend Forwardable
|
20
|
+
|
19
21
|
def self.from_client(response)
|
20
22
|
encoder = WebAuthn.configuration.encoder
|
21
23
|
|
@@ -48,8 +50,6 @@ module WebAuthn
|
|
48
50
|
@attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes)
|
49
51
|
end
|
50
52
|
|
51
|
-
extend Forwardable
|
52
|
-
|
53
53
|
def_delegators(
|
54
54
|
:attestation_object,
|
55
55
|
:aaguid,
|
@@ -16,11 +16,7 @@ module WebAuthn
|
|
16
16
|
class RootCertificateFinderNotSupportedError < Error; end
|
17
17
|
|
18
18
|
class Configuration
|
19
|
-
|
20
|
-
OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil
|
21
|
-
end
|
22
|
-
|
23
|
-
DEFAULT_ALGORITHMS = ["ES256", if_pss_supported("PS256"), "RS256"].compact.freeze
|
19
|
+
DEFAULT_ALGORITHMS = ["ES256", "PS256", "RS256"].compact.freeze
|
24
20
|
|
25
21
|
attr_accessor :algorithms
|
26
22
|
attr_accessor :encoding
|
@@ -39,7 +35,7 @@ module WebAuthn
|
|
39
35
|
@verify_attestation_statement = true
|
40
36
|
@credential_options_timeout = 120000
|
41
37
|
@silent_authentication = false
|
42
|
-
@acceptable_attestation_types = ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA']
|
38
|
+
@acceptable_attestation_types = ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA', 'AnonCA']
|
43
39
|
@attestation_root_certificates_finders = []
|
44
40
|
end
|
45
41
|
|