webauthn 2.0.0.beta1 → 2.3.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/.gitignore +1 -0
- data/.rubocop.yml +65 -13
- data/.travis.yml +22 -18
- data/Appraisals +4 -0
- data/CHANGELOG.md +72 -25
- data/CONTRIBUTING.md +0 -5
- data/README.md +172 -15
- data/SECURITY.md +4 -4
- data/gemfiles/openssl_2_2.gemfile +7 -0
- data/lib/cose/rsapkcs1_algorithm.rb +43 -0
- data/lib/webauthn/attestation_object.rb +43 -0
- data/lib/webauthn/attestation_statement.rb +20 -20
- data/lib/webauthn/attestation_statement/android_key.rb +28 -30
- data/lib/webauthn/attestation_statement/android_safetynet.rb +30 -20
- data/lib/webauthn/attestation_statement/base.rb +124 -14
- data/lib/webauthn/attestation_statement/fido_u2f.rb +13 -9
- data/lib/webauthn/attestation_statement/packed.rb +14 -42
- data/lib/webauthn/attestation_statement/tpm.rb +38 -54
- data/lib/webauthn/authenticator_assertion_response.rb +7 -36
- data/lib/webauthn/authenticator_attestation_response.rb +24 -46
- data/lib/webauthn/authenticator_data.rb +51 -51
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +29 -50
- data/lib/webauthn/authenticator_response.rb +15 -10
- data/lib/webauthn/configuration.rb +23 -0
- data/lib/webauthn/credential.rb +4 -4
- data/lib/webauthn/credential_creation_options.rb +1 -1
- data/lib/webauthn/fake_authenticator.rb +7 -3
- data/lib/webauthn/fake_authenticator/attestation_object.rb +7 -3
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +2 -4
- data/lib/webauthn/fake_client.rb +17 -4
- data/lib/webauthn/public_key.rb +68 -0
- data/lib/webauthn/public_key_credential.rb +13 -3
- data/lib/webauthn/public_key_credential/creation_options.rb +2 -2
- data/lib/webauthn/u2f_migrator.rb +5 -4
- data/lib/webauthn/version.rb +1 -1
- data/script/ci/install-openssl +7 -0
- data/script/ci/install-ruby +13 -0
- data/webauthn.gemspec +14 -9
- metadata +70 -42
- data/lib/android_safetynet/attestation_response.rb +0 -84
- data/lib/cose/algorithm.rb +0 -38
- data/lib/tpm/constants.rb +0 -22
- data/lib/tpm/s_attest.rb +0 -26
- data/lib/tpm/s_attest/s_certify_info.rb +0 -14
- data/lib/tpm/sized_buffer.rb +0 -13
- data/lib/tpm/t_public.rb +0 -32
- data/lib/tpm/t_public/s_ecc_parms.rb +0 -17
- data/lib/tpm/t_public/s_rsa_parms.rb +0 -17
- data/lib/webauthn/attestation_statement/android_key/authorization_list.rb +0 -39
- data/lib/webauthn/attestation_statement/android_key/key_description.rb +0 -37
- data/lib/webauthn/attestation_statement/tpm/cert_info.rb +0 -44
- data/lib/webauthn/attestation_statement/tpm/pub_area.rb +0 -85
- data/lib/webauthn/signature_verifier.rb +0 -65
data/SECURITY.md
CHANGED
@@ -4,11 +4,11 @@
|
|
4
4
|
|
5
5
|
| Version | Supported |
|
6
6
|
| ------- | ------------------ |
|
7
|
+
| 2.2.z | :white_check_mark: |
|
8
|
+
| 2.1.z | :white_check_mark: |
|
9
|
+
| 2.0.z | :white_check_mark: |
|
7
10
|
| 1.18.z | :white_check_mark: |
|
8
|
-
| 1.
|
9
|
-
| 1.16.z | :white_check_mark: |
|
10
|
-
| 1.15.z | :white_check_mark: |
|
11
|
-
| < 1.15 | :x: |
|
11
|
+
| < 1.18 | :x: |
|
12
12
|
|
13
13
|
## Reporting a Vulnerability
|
14
14
|
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cose"
|
4
|
+
require "cose/algorithm/signature_algorithm"
|
5
|
+
require "cose/error"
|
6
|
+
require "cose/key/rsa"
|
7
|
+
require "openssl/signature_algorithm/rsapkcs1"
|
8
|
+
|
9
|
+
class RSAPKCS1Algorithm < COSE::Algorithm::SignatureAlgorithm
|
10
|
+
attr_reader :hash_function
|
11
|
+
|
12
|
+
def initialize(*args, hash_function:)
|
13
|
+
super(*args)
|
14
|
+
|
15
|
+
@hash_function = hash_function
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def signature_algorithm_class
|
21
|
+
OpenSSL::SignatureAlgorithm::RSAPKCS1
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid_key?(key)
|
25
|
+
to_cose_key(key).is_a?(COSE::Key::RSA)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_pkey(key)
|
29
|
+
case key
|
30
|
+
when COSE::Key::RSA
|
31
|
+
key.to_pkey
|
32
|
+
when OpenSSL::PKey::RSA
|
33
|
+
key
|
34
|
+
else
|
35
|
+
raise(COSE::Error, "Incompatible key for algorithm")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
COSE::Algorithm.register(RSAPKCS1Algorithm.new(-257, "RS256", hash_function: "SHA256"))
|
41
|
+
COSE::Algorithm.register(RSAPKCS1Algorithm.new(-258, "RS384", hash_function: "SHA384"))
|
42
|
+
COSE::Algorithm.register(RSAPKCS1Algorithm.new(-259, "RS512", hash_function: "SHA512"))
|
43
|
+
COSE::Algorithm.register(RSAPKCS1Algorithm.new(-65535, "RS1", hash_function: "SHA1"))
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cbor"
|
4
|
+
require "forwardable"
|
5
|
+
require "openssl"
|
6
|
+
require "webauthn/attestation_statement"
|
7
|
+
require "webauthn/authenticator_data"
|
8
|
+
|
9
|
+
module WebAuthn
|
10
|
+
class AttestationObject
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
def self.deserialize(attestation_object)
|
14
|
+
from_map(CBOR.decode(attestation_object))
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.from_map(map)
|
18
|
+
new(
|
19
|
+
authenticator_data: WebAuthn::AuthenticatorData.deserialize(map["authData"]),
|
20
|
+
attestation_statement: WebAuthn::AttestationStatement.from(map["fmt"], map["attStmt"])
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :authenticator_data, :attestation_statement
|
25
|
+
|
26
|
+
def initialize(authenticator_data:, attestation_statement:)
|
27
|
+
@authenticator_data = authenticator_data
|
28
|
+
@attestation_statement = attestation_statement
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid_attested_credential?
|
32
|
+
authenticator_data.attested_credential_data_included? &&
|
33
|
+
authenticator_data.attested_credential_data.valid?
|
34
|
+
end
|
35
|
+
|
36
|
+
def valid_attestation_statement?(client_data_hash)
|
37
|
+
attestation_statement.valid?(authenticator_data, client_data_hash)
|
38
|
+
end
|
39
|
+
|
40
|
+
def_delegators :authenticator_data, :credential, :aaguid
|
41
|
+
def_delegators :attestation_statement, :attestation_certificate_key_id
|
42
|
+
end
|
43
|
+
end
|
@@ -1,5 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "webauthn/attestation_statement/android_key"
|
4
|
+
require "webauthn/attestation_statement/android_safetynet"
|
5
|
+
require "webauthn/attestation_statement/fido_u2f"
|
6
|
+
require "webauthn/attestation_statement/none"
|
7
|
+
require "webauthn/attestation_statement/packed"
|
8
|
+
require "webauthn/attestation_statement/tpm"
|
3
9
|
require "webauthn/error"
|
4
10
|
|
5
11
|
module WebAuthn
|
@@ -13,28 +19,22 @@ module WebAuthn
|
|
13
19
|
ATTESTATION_FORMAT_ANDROID_KEY = "android-key"
|
14
20
|
ATTESTATION_FORMAT_TPM = "tpm"
|
15
21
|
|
22
|
+
FORMAT_TO_CLASS = {
|
23
|
+
ATTESTATION_FORMAT_NONE => WebAuthn::AttestationStatement::None,
|
24
|
+
ATTESTATION_FORMAT_FIDO_U2F => WebAuthn::AttestationStatement::FidoU2f,
|
25
|
+
ATTESTATION_FORMAT_PACKED => WebAuthn::AttestationStatement::Packed,
|
26
|
+
ATTESTATION_FORMAT_ANDROID_SAFETYNET => WebAuthn::AttestationStatement::AndroidSafetynet,
|
27
|
+
ATTESTATION_FORMAT_ANDROID_KEY => WebAuthn::AttestationStatement::AndroidKey,
|
28
|
+
ATTESTATION_FORMAT_TPM => WebAuthn::AttestationStatement::TPM
|
29
|
+
}.freeze
|
30
|
+
|
16
31
|
def self.from(format, statement)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
when ATTESTATION_FORMAT_FIDO_U2F
|
22
|
-
require "webauthn/attestation_statement/fido_u2f"
|
23
|
-
WebAuthn::AttestationStatement::FidoU2f.new(statement)
|
24
|
-
when ATTESTATION_FORMAT_PACKED
|
25
|
-
require "webauthn/attestation_statement/packed"
|
26
|
-
WebAuthn::AttestationStatement::Packed.new(statement)
|
27
|
-
when ATTESTATION_FORMAT_ANDROID_SAFETYNET
|
28
|
-
require "webauthn/attestation_statement/android_safetynet"
|
29
|
-
WebAuthn::AttestationStatement::AndroidSafetynet.new(statement)
|
30
|
-
when ATTESTATION_FORMAT_ANDROID_KEY
|
31
|
-
require "webauthn/attestation_statement/android_key"
|
32
|
-
WebAuthn::AttestationStatement::AndroidKey.new(statement)
|
33
|
-
when ATTESTATION_FORMAT_TPM
|
34
|
-
require "webauthn/attestation_statement/tpm"
|
35
|
-
WebAuthn::AttestationStatement::TPM.new(statement)
|
32
|
+
klass = FORMAT_TO_CLASS[format]
|
33
|
+
|
34
|
+
if klass
|
35
|
+
klass.new(statement)
|
36
36
|
else
|
37
|
-
raise
|
37
|
+
raise(FormatNotSupportedError, "Unsupported attestation format '#{format}'")
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
@@ -1,73 +1,71 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "android_key_attestation"
|
3
4
|
require "openssl"
|
4
|
-
require "webauthn/attestation_statement/android_key/key_description"
|
5
5
|
require "webauthn/attestation_statement/base"
|
6
|
-
require "webauthn/security_utils"
|
7
|
-
require "webauthn/signature_verifier"
|
8
6
|
|
9
7
|
module WebAuthn
|
10
8
|
module AttestationStatement
|
11
9
|
class AndroidKey < Base
|
12
|
-
EXTENSION_DATA_OID = "1.3.6.1.4.1.11129.2.1.17"
|
13
|
-
|
14
|
-
# https://android.googlesource.com/platform/hardware/libhardware/+/master/include/hardware/keymaster_defs.h
|
15
|
-
KM_ORIGIN_GENERATED = 0
|
16
|
-
KM_PURPOSE_SIGN = 2
|
17
|
-
|
18
10
|
def valid?(authenticator_data, client_data_hash)
|
19
11
|
valid_signature?(authenticator_data, client_data_hash) &&
|
20
12
|
matching_public_key?(authenticator_data) &&
|
21
13
|
valid_attestation_challenge?(client_data_hash) &&
|
22
|
-
|
14
|
+
all_applications_fields_not_set? &&
|
23
15
|
valid_authorization_list_origin? &&
|
24
16
|
valid_authorization_list_purpose? &&
|
25
|
-
|
17
|
+
trustworthy?(aaguid: authenticator_data.aaguid) &&
|
18
|
+
[attestation_type, attestation_trust_path]
|
26
19
|
end
|
27
20
|
|
28
21
|
private
|
29
22
|
|
30
|
-
def valid_signature?(authenticator_data, client_data_hash)
|
31
|
-
WebAuthn::SignatureVerifier
|
32
|
-
.new(algorithm, attestation_certificate.public_key)
|
33
|
-
.verify(signature, authenticator_data.data + client_data_hash)
|
34
|
-
end
|
35
|
-
|
36
23
|
def matching_public_key?(authenticator_data)
|
37
24
|
attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der
|
38
25
|
end
|
39
26
|
|
40
27
|
def valid_attestation_challenge?(client_data_hash)
|
41
|
-
|
28
|
+
android_key_attestation.verify_challenge(client_data_hash)
|
29
|
+
rescue AndroidKeyAttestation::ChallengeMismatchError
|
30
|
+
false
|
42
31
|
end
|
43
32
|
|
44
|
-
def
|
45
|
-
|
33
|
+
def valid_certificate_chain?(aaguid: nil, **_)
|
34
|
+
android_key_attestation.verify_certificate_chain(root_certificates: root_certificates(aaguid: aaguid))
|
35
|
+
rescue AndroidKeyAttestation::CertificateVerificationError
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
def all_applications_fields_not_set?
|
40
|
+
!tee_enforced.all_applications && !software_enforced.all_applications
|
46
41
|
end
|
47
42
|
|
48
43
|
def valid_authorization_list_origin?
|
49
|
-
tee_enforced.origin ==
|
44
|
+
tee_enforced.origin == :generated || software_enforced.origin == :generated
|
50
45
|
end
|
51
46
|
|
52
47
|
def valid_authorization_list_purpose?
|
53
|
-
tee_enforced.purpose ==
|
48
|
+
tee_enforced.purpose == [:sign] || software_enforced.purpose == [:sign]
|
54
49
|
end
|
55
50
|
|
56
51
|
def tee_enforced
|
57
|
-
|
52
|
+
android_key_attestation.tee_enforced
|
58
53
|
end
|
59
54
|
|
60
55
|
def software_enforced
|
61
|
-
|
56
|
+
android_key_attestation.software_enforced
|
57
|
+
end
|
58
|
+
|
59
|
+
def attestation_type
|
60
|
+
WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC
|
62
61
|
end
|
63
62
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
67
|
-
raw_key_description = OpenSSL::ASN1.decode(extension_data).value.last
|
63
|
+
def default_root_certificates
|
64
|
+
AndroidKeyAttestation::Statement::GOOGLE_ROOT_CERTIFICATES
|
65
|
+
end
|
68
66
|
|
69
|
-
|
70
|
-
|
67
|
+
def android_key_attestation
|
68
|
+
@android_key_attestation ||= AndroidKeyAttestation::Statement.new(*certificates)
|
71
69
|
end
|
72
70
|
end
|
73
71
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "safety_net_attestation"
|
4
4
|
require "openssl"
|
5
5
|
require "webauthn/attestation_statement/base"
|
6
6
|
|
@@ -8,35 +8,27 @@ module WebAuthn
|
|
8
8
|
module AttestationStatement
|
9
9
|
# Implements https://www.w3.org/TR/webauthn-1/#sctn-android-safetynet-attestation
|
10
10
|
class AndroidSafetynet < Base
|
11
|
-
def
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
def valid?(authenticator_data, client_data_hash, trust_store: self.class.default_trust_store)
|
16
|
-
trusted_attestation_certificate?(trust_store) &&
|
17
|
-
valid_response?(authenticator_data, client_data_hash) &&
|
11
|
+
def valid?(authenticator_data, client_data_hash)
|
12
|
+
valid_response?(authenticator_data, client_data_hash) &&
|
18
13
|
valid_version? &&
|
19
14
|
cts_profile_match? &&
|
20
|
-
|
15
|
+
trustworthy?(aaguid: authenticator_data.aaguid) &&
|
16
|
+
[attestation_type, attestation_trust_path]
|
21
17
|
end
|
22
18
|
|
23
19
|
def attestation_certificate
|
24
|
-
|
20
|
+
attestation_trust_path.first
|
25
21
|
end
|
26
22
|
|
27
23
|
private
|
28
24
|
|
29
|
-
# FIXME: This should be a responsibility of AndroidSafetynet::AttestationResponse#verify
|
30
|
-
def trusted_attestation_certificate?(trust_store)
|
31
|
-
trust_store.verify(attestation_certificate, signing_certificates)
|
32
|
-
end
|
33
|
-
|
34
25
|
def valid_response?(authenticator_data, client_data_hash)
|
35
26
|
nonce = Digest::SHA256.base64digest(authenticator_data.data + client_data_hash)
|
36
27
|
|
37
28
|
begin
|
38
|
-
attestation_response
|
39
|
-
|
29
|
+
attestation_response
|
30
|
+
.verify(nonce, trusted_certificates: root_certificates(aaguid: authenticator_data.aaguid), time: time)
|
31
|
+
rescue SafetyNetAttestation::Error
|
40
32
|
false
|
41
33
|
end
|
42
34
|
end
|
@@ -50,12 +42,30 @@ module WebAuthn
|
|
50
42
|
attestation_response.cts_profile_match?
|
51
43
|
end
|
52
44
|
|
53
|
-
def
|
54
|
-
|
45
|
+
def valid_certificate_chain?(**_)
|
46
|
+
# Already performed as part of #valid_response?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def attestation_type
|
51
|
+
WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC
|
52
|
+
end
|
53
|
+
|
54
|
+
# SafetyNetAttestation returns full chain including root, WebAuthn expects only the x5c certificates
|
55
|
+
def attestation_trust_path
|
56
|
+
attestation_response.certificate_chain[0..-2]
|
55
57
|
end
|
56
58
|
|
57
59
|
def attestation_response
|
58
|
-
@attestation_response ||= ::
|
60
|
+
@attestation_response ||= SafetyNetAttestation::Statement.new(statement["response"])
|
61
|
+
end
|
62
|
+
|
63
|
+
def default_root_certificates
|
64
|
+
SafetyNetAttestation::Statement::GOOGLE_ROOT_CERTIFICATES
|
65
|
+
end
|
66
|
+
|
67
|
+
def time
|
68
|
+
Time.now
|
59
69
|
end
|
60
70
|
end
|
61
71
|
end
|
@@ -1,21 +1,29 @@
|
|
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"
|
15
19
|
|
16
|
-
|
17
|
-
|
20
|
+
ATTESTATION_TYPES_WITH_ROOT = [
|
21
|
+
ATTESTATION_TYPE_BASIC,
|
22
|
+
ATTESTATION_TYPE_BASIC_OR_ATTCA,
|
23
|
+
ATTESTATION_TYPE_ATTCA
|
24
|
+
].freeze
|
18
25
|
|
26
|
+
class Base
|
19
27
|
AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4"
|
20
28
|
|
21
29
|
def initialize(statement)
|
@@ -23,11 +31,25 @@ module WebAuthn
|
|
23
31
|
end
|
24
32
|
|
25
33
|
def valid?(_authenticator_data, _client_data_hash)
|
26
|
-
raise
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
def format
|
38
|
+
WebAuthn::AttestationStatement::FORMAT_TO_CLASS.key(self.class)
|
27
39
|
end
|
28
40
|
|
29
41
|
def attestation_certificate
|
30
|
-
|
42
|
+
certificates&.first
|
43
|
+
end
|
44
|
+
|
45
|
+
def certificate_chain
|
46
|
+
if certificates
|
47
|
+
certificates[1..-1]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def attestation_certificate_key_id
|
52
|
+
raw_subject_key_identifier&.unpack("H*")&.[](0)
|
31
53
|
end
|
32
54
|
|
33
55
|
private
|
@@ -46,27 +68,115 @@ module WebAuthn
|
|
46
68
|
end
|
47
69
|
end
|
48
70
|
|
49
|
-
def
|
50
|
-
@
|
51
|
-
|
52
|
-
|
71
|
+
def certificates
|
72
|
+
@certificates ||=
|
73
|
+
raw_certificates&.map do |raw_certificate|
|
74
|
+
OpenSSL::X509::Certificate.new(raw_certificate)
|
75
|
+
end
|
53
76
|
end
|
54
77
|
|
55
78
|
def algorithm
|
56
79
|
statement["alg"]
|
57
80
|
end
|
58
81
|
|
59
|
-
def
|
82
|
+
def raw_certificates
|
60
83
|
statement["x5c"]
|
61
84
|
end
|
62
85
|
|
63
|
-
def raw_ecdaa_key_id
|
64
|
-
statement["ecdaaKeyId"]
|
65
|
-
end
|
66
|
-
|
67
86
|
def signature
|
68
87
|
statement["sig"]
|
69
88
|
end
|
89
|
+
|
90
|
+
def attestation_trust_path
|
91
|
+
if certificates&.any?
|
92
|
+
certificates
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def trustworthy?(aaguid: nil, attestation_certificate_key_id: nil)
|
97
|
+
if ATTESTATION_TYPES_WITH_ROOT.include?(attestation_type)
|
98
|
+
configuration.acceptable_attestation_types.include?(attestation_type) &&
|
99
|
+
valid_certificate_chain?(aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id)
|
100
|
+
else
|
101
|
+
configuration.acceptable_attestation_types.include?(attestation_type)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def valid_certificate_chain?(aaguid: nil, attestation_certificate_key_id: nil)
|
106
|
+
attestation_root_certificates_store(
|
107
|
+
aaguid: aaguid,
|
108
|
+
attestation_certificate_key_id: attestation_certificate_key_id
|
109
|
+
).verify(attestation_certificate, attestation_trust_path)
|
110
|
+
end
|
111
|
+
|
112
|
+
def attestation_root_certificates_store(aaguid: nil, attestation_certificate_key_id: nil)
|
113
|
+
OpenSSL::X509::Store.new.tap do |store|
|
114
|
+
root_certificates(
|
115
|
+
aaguid: aaguid,
|
116
|
+
attestation_certificate_key_id: attestation_certificate_key_id
|
117
|
+
).each do |cert|
|
118
|
+
store.add_cert(cert)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def root_certificates(aaguid: nil, attestation_certificate_key_id: nil)
|
124
|
+
root_certificates =
|
125
|
+
configuration.attestation_root_certificates_finders.reduce([]) do |certs, finder|
|
126
|
+
if certs.empty?
|
127
|
+
finder.find(
|
128
|
+
attestation_format: format,
|
129
|
+
aaguid: aaguid,
|
130
|
+
attestation_certificate_key_id: attestation_certificate_key_id
|
131
|
+
) || []
|
132
|
+
else
|
133
|
+
certs
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
if root_certificates.empty? && respond_to?(:default_root_certificates, true)
|
138
|
+
default_root_certificates
|
139
|
+
else
|
140
|
+
root_certificates
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
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
|
+
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
|
+
|
177
|
+
def configuration
|
178
|
+
WebAuthn.configuration
|
179
|
+
end
|
70
180
|
end
|
71
181
|
end
|
72
182
|
end
|