webauthn 1.18.0 → 2.0.0.beta1
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/.rubocop.yml +8 -0
- data/.travis.yml +7 -3
- data/Appraisals +8 -0
- data/CHANGELOG.md +52 -0
- data/README.md +88 -80
- data/SECURITY.md +18 -0
- data/gemfiles/cose_head.gemfile +7 -0
- data/gemfiles/openssl_head.gemfile +7 -0
- data/lib/webauthn.rb +9 -1
- data/lib/webauthn/attestation_statement/android_safetynet.rb +4 -4
- data/lib/webauthn/attestation_statement/base.rb +4 -4
- data/lib/webauthn/attestation_statement/fido_u2f.rb +1 -2
- data/lib/webauthn/authenticator_assertion_response.rb +33 -35
- data/lib/webauthn/authenticator_attestation_response.rb +30 -0
- data/lib/webauthn/authenticator_data.rb +3 -1
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +1 -0
- data/lib/webauthn/authenticator_response.rb +1 -2
- data/lib/webauthn/client_data.rb +2 -1
- data/lib/webauthn/configuration.rb +9 -0
- data/lib/webauthn/credential.rb +26 -0
- data/lib/webauthn/credential_creation_options.rb +5 -1
- data/lib/webauthn/credential_request_options.rb +5 -0
- data/lib/webauthn/encoder.rb +8 -1
- data/lib/webauthn/fake_authenticator.rb +1 -0
- data/lib/webauthn/fake_client.rb +26 -22
- data/lib/webauthn/public_key_credential.rb +10 -50
- data/lib/webauthn/public_key_credential/creation_options.rb +92 -0
- data/lib/webauthn/public_key_credential/entity.rb +44 -0
- data/lib/webauthn/public_key_credential/options.rb +72 -0
- data/lib/webauthn/public_key_credential/request_options.rb +36 -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_with_assertion.rb +35 -0
- data/lib/webauthn/public_key_credential_with_attestation.rb +30 -0
- data/lib/webauthn/u2f_migrator.rb +1 -1
- data/lib/webauthn/version.rb +1 -1
- data/webauthn.gemspec +3 -2
- metadata +33 -8
- data/webauthn-ruby.png +0 -0
@@ -10,7 +10,6 @@ module WebAuthn
|
|
10
10
|
class FidoU2f < Base
|
11
11
|
VALID_ATTESTATION_CERTIFICATE_COUNT = 1
|
12
12
|
VALID_ATTESTATION_CERTIFICATE_ALGORITHM = COSE::Algorithm.by_name("ES256")
|
13
|
-
VALID_ATTESTED_AAGUID = 0.chr * WebAuthn::AuthenticatorData::AttestedCredentialData::AAGUID_LENGTH
|
14
13
|
|
15
14
|
def valid?(authenticator_data, client_data_hash)
|
16
15
|
valid_format? &&
|
@@ -43,7 +42,7 @@ module WebAuthn
|
|
43
42
|
end
|
44
43
|
|
45
44
|
def valid_aaguid?(attested_credential_data_aaguid)
|
46
|
-
attested_credential_data_aaguid ==
|
45
|
+
attested_credential_data_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID
|
47
46
|
end
|
48
47
|
|
49
48
|
def valid_signature?(authenticator_data, client_data_hash)
|
@@ -5,32 +5,45 @@ require "cose/key"
|
|
5
5
|
require "webauthn/attestation_statement/fido_u2f/public_key"
|
6
6
|
require "webauthn/authenticator_data"
|
7
7
|
require "webauthn/authenticator_response"
|
8
|
+
require "webauthn/encoder"
|
8
9
|
require "webauthn/signature_verifier"
|
9
10
|
|
10
11
|
module WebAuthn
|
11
|
-
class CredentialVerificationError < VerificationError; end
|
12
12
|
class SignatureVerificationError < VerificationError; end
|
13
13
|
class SignCountVerificationError < VerificationError; end
|
14
14
|
|
15
15
|
class AuthenticatorAssertionResponse < AuthenticatorResponse
|
16
|
+
def self.from_client(response)
|
17
|
+
encoder = WebAuthn.configuration.encoder
|
18
|
+
|
19
|
+
user_handle =
|
20
|
+
if response["userHandle"]
|
21
|
+
encoder.decode(response["userHandle"])
|
22
|
+
end
|
23
|
+
|
24
|
+
new(
|
25
|
+
authenticator_data: encoder.decode(response["authenticatorData"]),
|
26
|
+
client_data_json: encoder.decode(response["clientDataJSON"]),
|
27
|
+
signature: encoder.decode(response["signature"]),
|
28
|
+
user_handle: user_handle
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
16
32
|
attr_reader :user_handle
|
17
33
|
|
18
|
-
|
19
|
-
def initialize(credential_id:, authenticator_data:, signature:, user_handle: nil, **options)
|
34
|
+
def initialize(authenticator_data:, signature:, user_handle: nil, **options)
|
20
35
|
super(options)
|
21
36
|
|
22
|
-
@credential_id = credential_id
|
23
37
|
@authenticator_data_bytes = authenticator_data
|
24
38
|
@signature = signature
|
25
39
|
@user_handle = user_handle
|
26
40
|
end
|
27
41
|
|
28
|
-
def verify(expected_challenge, expected_origin = nil,
|
42
|
+
def verify(expected_challenge, expected_origin = nil, public_key:, sign_count:, user_verification: nil,
|
43
|
+
rp_id: nil)
|
29
44
|
super(expected_challenge, expected_origin, user_verification: user_verification, rp_id: rp_id)
|
30
|
-
|
31
|
-
verify_item(:
|
32
|
-
verify_item(:signature, credential_cose_key(allowed_credentials))
|
33
|
-
verify_item(:sign_count, allowed_credentials)
|
45
|
+
verify_item(:signature, credential_cose_key(public_key))
|
46
|
+
verify_item(:sign_count, sign_count)
|
34
47
|
|
35
48
|
true
|
36
49
|
end
|
@@ -41,7 +54,7 @@ module WebAuthn
|
|
41
54
|
|
42
55
|
private
|
43
56
|
|
44
|
-
attr_reader :
|
57
|
+
attr_reader :authenticator_data_bytes, :signature
|
45
58
|
|
46
59
|
def valid_signature?(credential_cose_key)
|
47
60
|
WebAuthn::SignatureVerifier
|
@@ -49,32 +62,17 @@ module WebAuthn
|
|
49
62
|
.verify(signature, authenticator_data_bytes + client_data.hash)
|
50
63
|
end
|
51
64
|
|
52
|
-
def valid_sign_count?(
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
# TODO: make passing sign count mandatory in next major version
|
57
|
-
stored_sign_count = matched_credential.fetch(:sign_count, 0)
|
58
|
-
|
59
|
-
if authenticator_data.sign_count.nonzero? || stored_sign_count.nonzero?
|
60
|
-
authenticator_data.sign_count > stored_sign_count
|
65
|
+
def valid_sign_count?(stored_sign_count)
|
66
|
+
normalized_sign_count = stored_sign_count || 0
|
67
|
+
if authenticator_data.sign_count.nonzero? || normalized_sign_count.nonzero?
|
68
|
+
authenticator_data.sign_count > normalized_sign_count
|
61
69
|
else
|
62
70
|
true
|
63
71
|
end
|
64
72
|
end
|
65
73
|
|
66
|
-
def
|
67
|
-
|
68
|
-
|
69
|
-
allowed_credential_ids.include?(credential_id)
|
70
|
-
end
|
71
|
-
|
72
|
-
def credential_cose_key(allowed_credentials)
|
73
|
-
matched_credential = allowed_credentials.find do |credential|
|
74
|
-
credential[:id] == credential_id
|
75
|
-
end
|
76
|
-
|
77
|
-
if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(matched_credential[:public_key])
|
74
|
+
def credential_cose_key(public_key)
|
75
|
+
if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(public_key)
|
78
76
|
# Gem version v1.11.0 and lower, used to behave so that Credential#public_key
|
79
77
|
# returned an EC P-256 uncompressed point.
|
80
78
|
#
|
@@ -83,16 +81,16 @@ module WebAuthn
|
|
83
81
|
# credentialPublicKey (as in https://www.w3.org/TR/webauthn/#credentialpublickey).
|
84
82
|
#
|
85
83
|
# Given that the credential public key is expected to be stored long-term by the gem
|
86
|
-
# user and later be passed as
|
84
|
+
# user and later be passed as the public_key argument in the
|
87
85
|
# AuthenticatorAssertionResponse.verify call, we then need to support the two formats.
|
88
86
|
COSE::Key::EC2.new(
|
89
87
|
alg: COSE::Algorithm.by_name("ES256").id,
|
90
88
|
crv: 1,
|
91
|
-
x:
|
92
|
-
y:
|
89
|
+
x: public_key[1..32],
|
90
|
+
y: public_key[33..-1]
|
93
91
|
)
|
94
92
|
else
|
95
|
-
COSE::Key.deserialize(
|
93
|
+
COSE::Key.deserialize(public_key)
|
96
94
|
end
|
97
95
|
end
|
98
96
|
|
@@ -8,12 +8,22 @@ require "webauthn/authenticator_data"
|
|
8
8
|
require "webauthn/authenticator_response"
|
9
9
|
require "webauthn/attestation_statement"
|
10
10
|
require "webauthn/client_data"
|
11
|
+
require "webauthn/encoder"
|
11
12
|
|
12
13
|
module WebAuthn
|
13
14
|
class AttestationStatementVerificationError < VerificationError; end
|
14
15
|
class AttestedCredentialVerificationError < VerificationError; end
|
15
16
|
|
16
17
|
class AuthenticatorAttestationResponse < AuthenticatorResponse
|
18
|
+
def self.from_client(response)
|
19
|
+
encoder = WebAuthn.configuration.encoder
|
20
|
+
|
21
|
+
new(
|
22
|
+
attestation_object: encoder.decode(response["attestationObject"]),
|
23
|
+
client_data_json: encoder.decode(response["clientDataJSON"])
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
17
27
|
attr_reader :attestation_type, :attestation_trust_path
|
18
28
|
|
19
29
|
def initialize(attestation_object:, **options)
|
@@ -52,6 +62,17 @@ module WebAuthn
|
|
52
62
|
@attestation ||= CBOR.decode(attestation_object)
|
53
63
|
end
|
54
64
|
|
65
|
+
def aaguid
|
66
|
+
raw_aaguid = authenticator_data.attested_credential_data.raw_aaguid
|
67
|
+
unless raw_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID
|
68
|
+
authenticator_data.attested_credential_data.aaguid
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def attestation_certificate_key
|
73
|
+
raw_subject_key_identifier(attestation_statement.attestation_certificate)&.unpack("H*")&.[](0)
|
74
|
+
end
|
75
|
+
|
55
76
|
private
|
56
77
|
|
57
78
|
attr_reader :attestation_object
|
@@ -68,5 +89,14 @@ module WebAuthn
|
|
68
89
|
def valid_attestation_statement?
|
69
90
|
@attestation_type, @attestation_trust_path = attestation_statement.valid?(authenticator_data, client_data.hash)
|
70
91
|
end
|
92
|
+
|
93
|
+
def raw_subject_key_identifier(certificate)
|
94
|
+
extension = certificate.extensions.detect { |ext| ext.oid == "subjectKeyIdentifier" }
|
95
|
+
return unless extension
|
96
|
+
|
97
|
+
ext_asn1 = OpenSSL::ASN1.decode(extension.to_der)
|
98
|
+
ext_value = ext_asn1.value.last
|
99
|
+
OpenSSL::ASN1.decode(ext_value.value).value
|
100
|
+
end
|
71
101
|
end
|
72
102
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "base64"
|
4
3
|
require "webauthn/client_data"
|
5
4
|
require "webauthn/error"
|
6
5
|
require "webauthn/security_utils"
|
@@ -73,7 +72,7 @@ module WebAuthn
|
|
73
72
|
end
|
74
73
|
|
75
74
|
def valid_challenge?(expected_challenge)
|
76
|
-
WebAuthn::SecurityUtils.secure_compare(
|
75
|
+
WebAuthn::SecurityUtils.secure_compare(client_data.challenge, expected_challenge)
|
77
76
|
end
|
78
77
|
|
79
78
|
def valid_origin?(expected_origin)
|
data/lib/webauthn/client_data.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "json"
|
4
4
|
require "openssl"
|
5
|
+
require "webauthn/encoder"
|
5
6
|
require "webauthn/error"
|
6
7
|
|
7
8
|
module WebAuthn
|
@@ -19,7 +20,7 @@ module WebAuthn
|
|
19
20
|
end
|
20
21
|
|
21
22
|
def challenge
|
22
|
-
data["challenge"]
|
23
|
+
WebAuthn.standard_encoder.decode(data["challenge"])
|
23
24
|
end
|
24
25
|
|
25
26
|
def origin
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "openssl"
|
4
|
+
require "webauthn/encoder"
|
4
5
|
|
5
6
|
module WebAuthn
|
6
7
|
def self.configuration
|
@@ -19,6 +20,7 @@ module WebAuthn
|
|
19
20
|
DEFAULT_ALGORITHMS = ["ES256", if_pss_supported("PS256"), "RS256"].compact.freeze
|
20
21
|
|
21
22
|
attr_accessor :algorithms
|
23
|
+
attr_accessor :encoding
|
22
24
|
attr_accessor :origin
|
23
25
|
attr_accessor :rp_id
|
24
26
|
attr_accessor :rp_name
|
@@ -27,8 +29,15 @@ module WebAuthn
|
|
27
29
|
|
28
30
|
def initialize
|
29
31
|
@algorithms = DEFAULT_ALGORITHMS.dup
|
32
|
+
@encoding = WebAuthn::Encoder::STANDARD_ENCODING
|
30
33
|
@verify_attestation_statement = true
|
31
34
|
@credential_options_timeout = 120000
|
32
35
|
end
|
36
|
+
|
37
|
+
# This is the user-data encoder.
|
38
|
+
# Used to decode user input and to encode data provided to the user.
|
39
|
+
def encoder
|
40
|
+
@encoder ||= WebAuthn::Encoder.new(encoding)
|
41
|
+
end
|
33
42
|
end
|
34
43
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webauthn/public_key_credential/creation_options"
|
4
|
+
require "webauthn/public_key_credential/request_options"
|
5
|
+
require "webauthn/public_key_credential_with_assertion"
|
6
|
+
require "webauthn/public_key_credential_with_attestation"
|
7
|
+
|
8
|
+
module WebAuthn
|
9
|
+
module Credential
|
10
|
+
def self.options_for_create(*args)
|
11
|
+
WebAuthn::PublicKeyCredential::CreationOptions.new(*args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.options_for_get(*args)
|
15
|
+
WebAuthn::PublicKeyCredential::RequestOptions.new(*args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_create(credential)
|
19
|
+
WebAuthn::PublicKeyCredentialWithAttestation.from_client(credential)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.from_get(credential)
|
23
|
+
WebAuthn::PublicKeyCredentialWithAssertion.from_client(credential)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -6,8 +6,12 @@ require "webauthn/credential_rp_entity"
|
|
6
6
|
require "webauthn/credential_user_entity"
|
7
7
|
|
8
8
|
module WebAuthn
|
9
|
-
# TODO: make keyword arguments mandatory in next major version
|
10
9
|
def self.credential_creation_options(rp_name: nil, user_name: "web-user", display_name: "web-user", user_id: "1")
|
10
|
+
warn(
|
11
|
+
"DEPRECATION WARNING: `WebAuthn.credential_creation_options` is deprecated."\
|
12
|
+
" Please use `WebAuthn::Credential.options_for_create` instead."
|
13
|
+
)
|
14
|
+
|
11
15
|
CredentialCreationOptions.new(
|
12
16
|
rp_name: rp_name, user_id: user_id, user_name: user_name, user_display_name: display_name
|
13
17
|
).to_h
|
@@ -4,6 +4,11 @@ require "webauthn/credential_options"
|
|
4
4
|
|
5
5
|
module WebAuthn
|
6
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
|
+
|
7
12
|
CredentialRequestOptions.new.to_h
|
8
13
|
end
|
9
14
|
|
data/lib/webauthn/encoder.rb
CHANGED
@@ -3,10 +3,17 @@
|
|
3
3
|
require "base64"
|
4
4
|
|
5
5
|
module WebAuthn
|
6
|
+
def self.standard_encoder
|
7
|
+
@standard_encoder ||= Encoder.new
|
8
|
+
end
|
9
|
+
|
6
10
|
class Encoder
|
11
|
+
# https://www.w3.org/TR/webauthn-2/#base64url-encoding
|
12
|
+
STANDARD_ENCODING = :base64url
|
13
|
+
|
7
14
|
attr_reader :encoding
|
8
15
|
|
9
|
-
def initialize(encoding =
|
16
|
+
def initialize(encoding = STANDARD_ENCODING)
|
10
17
|
@encoding = encoding
|
11
18
|
end
|
12
19
|
|
data/lib/webauthn/fake_client.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "base64"
|
4
3
|
require "openssl"
|
4
|
+
require "securerandom"
|
5
5
|
require "webauthn/authenticator_data"
|
6
6
|
require "webauthn/encoder"
|
7
7
|
require "webauthn/fake_authenticator"
|
@@ -16,7 +16,7 @@ module WebAuthn
|
|
16
16
|
origin = fake_origin,
|
17
17
|
token_binding: nil,
|
18
18
|
authenticator: WebAuthn::FakeAuthenticator.new,
|
19
|
-
encoding:
|
19
|
+
encoding: WebAuthn.configuration.encoding
|
20
20
|
)
|
21
21
|
@origin = origin
|
22
22
|
@token_binding = token_binding
|
@@ -26,13 +26,14 @@ module WebAuthn
|
|
26
26
|
|
27
27
|
def create(
|
28
28
|
challenge: fake_challenge,
|
29
|
-
rp_id: nil,
|
29
|
+
rp_id: nil,
|
30
|
+
user_present: true,
|
30
31
|
user_verified: false,
|
31
32
|
attested_credential_data: true
|
32
33
|
)
|
33
34
|
rp_id ||= URI.parse(origin).host
|
34
35
|
|
35
|
-
client_data_json = data_json_for(:create, challenge)
|
36
|
+
client_data_json = data_json_for(:create, encoder.decode(challenge))
|
36
37
|
client_data_hash = hashed(client_data_json)
|
37
38
|
|
38
39
|
attestation_object = authenticator.make_credential(
|
@@ -50,14 +51,13 @@ module WebAuthn
|
|
50
51
|
"id-for-pk-without-attested-credential-data"
|
51
52
|
end
|
52
53
|
|
53
|
-
# TODO: return camelCase string keys instead of snakecase symbols
|
54
54
|
{
|
55
|
-
type
|
56
|
-
id
|
57
|
-
|
58
|
-
response
|
59
|
-
|
60
|
-
|
55
|
+
"type" => "public-key",
|
56
|
+
"id" => internal_encoder.encode(id),
|
57
|
+
"rawId" => encoder.encode(id),
|
58
|
+
"response" => {
|
59
|
+
"attestationObject" => encoder.encode(attestation_object),
|
60
|
+
"clientDataJSON" => encoder.encode(client_data_json)
|
61
61
|
}
|
62
62
|
}
|
63
63
|
end
|
@@ -65,7 +65,7 @@ module WebAuthn
|
|
65
65
|
def get(challenge: fake_challenge, rp_id: nil, user_present: true, user_verified: false, sign_count: nil)
|
66
66
|
rp_id ||= URI.parse(origin).host
|
67
67
|
|
68
|
-
client_data_json = data_json_for(:get, challenge)
|
68
|
+
client_data_json = data_json_for(:get, encoder.decode(challenge))
|
69
69
|
client_data_hash = hashed(client_data_json)
|
70
70
|
|
71
71
|
assertion = authenticator.get_assertion(
|
@@ -76,15 +76,15 @@ module WebAuthn
|
|
76
76
|
sign_count: sign_count,
|
77
77
|
)
|
78
78
|
|
79
|
-
# TODO: return camelCase string keys instead of snakecase symbols
|
80
79
|
{
|
81
|
-
type
|
82
|
-
id
|
83
|
-
|
84
|
-
response
|
85
|
-
|
86
|
-
|
87
|
-
signature
|
80
|
+
"type" => "public-key",
|
81
|
+
"id" => internal_encoder.encode(assertion[:credential_id]),
|
82
|
+
"rawId" => encoder.encode(assertion[:credential_id]),
|
83
|
+
"response" => {
|
84
|
+
"clientDataJSON" => encoder.encode(client_data_json),
|
85
|
+
"authenticatorData" => encoder.encode(assertion[:authenticator_data]),
|
86
|
+
"signature" => encoder.encode(assertion[:signature]),
|
87
|
+
"userHandle" => nil
|
88
88
|
}
|
89
89
|
}
|
90
90
|
end
|
@@ -96,7 +96,7 @@ module WebAuthn
|
|
96
96
|
def data_json_for(method, challenge)
|
97
97
|
data = {
|
98
98
|
type: type_for(method),
|
99
|
-
challenge:
|
99
|
+
challenge: internal_encoder.encode(challenge),
|
100
100
|
origin: origin
|
101
101
|
}
|
102
102
|
|
@@ -111,12 +111,16 @@ module WebAuthn
|
|
111
111
|
@encoder ||= WebAuthn::Encoder.new(encoding)
|
112
112
|
end
|
113
113
|
|
114
|
+
def internal_encoder
|
115
|
+
WebAuthn.standard_encoder
|
116
|
+
end
|
117
|
+
|
114
118
|
def hashed(data)
|
115
119
|
OpenSSL::Digest::SHA256.digest(data)
|
116
120
|
end
|
117
121
|
|
118
122
|
def fake_challenge
|
119
|
-
SecureRandom.random_bytes(32)
|
123
|
+
encoder.encode(SecureRandom.random_bytes(32))
|
120
124
|
end
|
121
125
|
|
122
126
|
def fake_origin
|