webauthn 1.13.0 → 1.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +7 -7
- data/lib/android_safetynet/attestation_response.rb +93 -0
- data/lib/cose/algorithm.rb +7 -4
- data/lib/tpm/constants.rb +21 -0
- data/lib/tpm/s_attest.rb +26 -0
- data/lib/tpm/s_attest/s_certify_info.rb +14 -0
- data/lib/tpm/sized_buffer.rb +13 -0
- data/lib/tpm/t_public.rb +32 -0
- data/lib/tpm/t_public/s_ecc_parms.rb +17 -0
- data/lib/tpm/t_public/s_rsa_parms.rb +17 -0
- data/lib/webauthn.rb +7 -2
- data/lib/webauthn/attestation_statement.rb +4 -0
- data/lib/webauthn/attestation_statement/android_key.rb +4 -34
- data/lib/webauthn/attestation_statement/android_safetynet.rb +14 -32
- data/lib/webauthn/attestation_statement/base.rb +39 -0
- data/lib/webauthn/attestation_statement/fido_u2f.rb +4 -17
- data/lib/webauthn/attestation_statement/packed.rb +11 -60
- data/lib/webauthn/attestation_statement/tpm.rb +97 -0
- data/lib/webauthn/attestation_statement/tpm/cert_info.rb +42 -0
- data/lib/webauthn/attestation_statement/tpm/pub_area.rb +82 -0
- data/lib/webauthn/authenticator_assertion_response.rb +6 -13
- data/lib/webauthn/authenticator_attestation_response.rb +1 -1
- data/lib/webauthn/authenticator_response.rb +10 -10
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +16 -9
- data/lib/webauthn/signature_verifier.rb +49 -0
- data/lib/webauthn/version.rb +1 -1
- data/webauthn.gemspec +1 -0
- metadata +28 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 812396778f9d74667ca7be0273e1ac38c0f0275aae8eb32b35f4d7aa5a52b6ec
|
4
|
+
data.tar.gz: 66539bc1f99c17b31f51e91df71b4423b059519eb0d8ccb7e4d637e6db8c588b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 875d8b449345498f08caf64f7a9b7cf01d537e85ac188d398ba337b2e13c24a2dd07c8013c201d0b00deb0eb191c8328abfed212e806afee287c91c63c6f8a46
|
7
|
+
data.tar.gz: fa8d32da1d05d15d9a74868c343cf2f907f31d9eab4830c1a3b4eead8317440e4ade8695ff19f463d494ee356a02486ffe37b5c52bf5beff3e70a78d4f4ccbd8
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v1.14.0] - 2019-04-25
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Support 'tpm' attestation statement
|
8
|
+
- Support RS256 credential public key
|
9
|
+
|
3
10
|
## [v1.13.0] - 2019-04-09
|
4
11
|
|
5
12
|
### Added
|
@@ -166,6 +173,7 @@ Note: Both additions should help making it compatible with Chrome for Android 70
|
|
166
173
|
- `WebAuthn::AuthenticatorAttestationResponse.valid?` can be used to validate fido-u2f attestations returned by the browser
|
167
174
|
- Works with ruby 2.5
|
168
175
|
|
176
|
+
[v1.14.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.13.0...v1.14.0/
|
169
177
|
[v1.13.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.12.0...v1.13.0/
|
170
178
|
[v1.12.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.11.0...v1.12.0/
|
171
179
|
[v1.11.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.10.0...v1.11.0/
|
data/README.md
CHANGED
@@ -93,15 +93,15 @@ attestation_response = WebAuthn::AuthenticatorAttestationResponse.new(
|
|
93
93
|
|
94
94
|
# This value needs to match `window.location.origin` evaluated by
|
95
95
|
# the User Agent as part of the verification phase.
|
96
|
-
|
96
|
+
expected_origin = "https://www.example.com"
|
97
97
|
|
98
|
-
# In the case that a Relying Party ID (https://www.w3.org/TR/webauthn/#relying-party-identifier) different from `
|
98
|
+
# In the case that a Relying Party ID (https://www.w3.org/TR/webauthn/#relying-party-identifier) different from `expected_origin` was used on
|
99
99
|
# `navigator.credentials.create`, it needs to specified for verification.
|
100
100
|
# Otherwise, you can ignore passing in this value to the `verify` method below.
|
101
101
|
rp_id = "example.com"
|
102
102
|
|
103
103
|
begin
|
104
|
-
attestation_response.verify(
|
104
|
+
attestation_response.verify(expected_challenge, expected_origin, rp_id: rp_id)
|
105
105
|
|
106
106
|
# 1. Register the new user and
|
107
107
|
# 2. Keep Credential ID and Credential Public Key under storage
|
@@ -160,9 +160,9 @@ assertion_response = WebAuthn::AuthenticatorAssertionResponse.new(
|
|
160
160
|
|
161
161
|
# This value needs to match `window.location.origin` evaluated by
|
162
162
|
# the User Agent as part of the verification phase.
|
163
|
-
|
163
|
+
expected_origin = "https://www.example.com"
|
164
164
|
|
165
|
-
# In the case that a Relying Party ID (https://www.w3.org/TR/webauthn/#relying-party-identifier) different from `
|
165
|
+
# In the case that a Relying Party ID (https://www.w3.org/TR/webauthn/#relying-party-identifier) different from `expected_origin` was used on
|
166
166
|
# `navigator.credentials.get`, it needs to be specified for verification.
|
167
167
|
# Otherwise, you can ignore passing in this value to the `verify` method below.`
|
168
168
|
rp_id = "example.com"
|
@@ -175,7 +175,7 @@ allowed_credential = {
|
|
175
175
|
}
|
176
176
|
|
177
177
|
begin
|
178
|
-
assertion_response.verify(
|
178
|
+
assertion_response.verify(expected_challenge, expected_origin, allowed_credentials: [allowed_credential], rp_id: rp_id)
|
179
179
|
|
180
180
|
# Sign in the user
|
181
181
|
rescue WebAuthn::VerificationError => e
|
@@ -190,7 +190,7 @@ end
|
|
190
190
|
| packed (self attestation) | Yes |
|
191
191
|
| packed (x5c attestation) | Yes |
|
192
192
|
| packed (ECDAA attestation) | No |
|
193
|
-
| tpm (x5c attestation) |
|
193
|
+
| tpm (x5c attestation) | Yes |
|
194
194
|
| tpm (ECDAA attestation) | No |
|
195
195
|
| android-key | Yes |
|
196
196
|
| android-safetynet | Yes |
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jwt"
|
4
|
+
|
5
|
+
module AndroidSafetynet
|
6
|
+
# Decoupled from WebAuthn, candidate for extraction
|
7
|
+
# Reference: https://developer.android.com/training/safetynet/attestation.html
|
8
|
+
class AttestationResponse
|
9
|
+
class VerificationError < StandardError; end
|
10
|
+
class LeafCertificateSubjectError < VerificationError; end
|
11
|
+
class NonceMismatchError < VerificationError; end
|
12
|
+
class SignatureError < VerificationError; end
|
13
|
+
class ResponseMissingError < VerificationError; end
|
14
|
+
|
15
|
+
CERTIRICATE_CHAIN_HEADER = "x5c"
|
16
|
+
VALID_SUBJECT_HOSTNAME = "attest.android.com"
|
17
|
+
HEADERS_POSITION = 1
|
18
|
+
PAYLOAD_POSITION = 0
|
19
|
+
|
20
|
+
attr_reader :response
|
21
|
+
|
22
|
+
def initialize(response)
|
23
|
+
@response = response
|
24
|
+
end
|
25
|
+
|
26
|
+
def verify(nonce)
|
27
|
+
if response
|
28
|
+
valid_nonce?(nonce) || raise(NonceMismatchError)
|
29
|
+
valid_attestation_domain? || raise(LeafCertificateSubjectError)
|
30
|
+
valid_signature? || raise(SignatureError)
|
31
|
+
else
|
32
|
+
raise(ResponseMissingError)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def cts_profile_match?
|
37
|
+
payload["ctsProfileMatch"]
|
38
|
+
end
|
39
|
+
|
40
|
+
def certificate_chain
|
41
|
+
@certificate_chain ||= headers[CERTIRICATE_CHAIN_HEADER].map do |cert|
|
42
|
+
OpenSSL::X509::Certificate.new(Base64.strict_decode64(cert))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def valid_nonce?(nonce)
|
49
|
+
payload["nonce"] == nonce
|
50
|
+
end
|
51
|
+
|
52
|
+
def valid_attestation_domain?
|
53
|
+
common_name = leaf_certificate&.subject&.to_a&.assoc('CN')
|
54
|
+
|
55
|
+
if common_name
|
56
|
+
common_name[1] == VALID_SUBJECT_HOSTNAME
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def valid_signature?
|
61
|
+
JWT.decode(response, leaf_certificate.public_key, true, algorithms: algorithm_for(leaf_certificate.public_key))
|
62
|
+
rescue JWT::VerificationError
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def algorithm_for(public_key)
|
67
|
+
case public_key
|
68
|
+
when OpenSSL::PKey::RSA
|
69
|
+
"RS256"
|
70
|
+
when OpenSSL::PKey::EC, OpenSSL::PKey::EC::Point
|
71
|
+
"ES256"
|
72
|
+
else
|
73
|
+
raise "Unsupported algorithm"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def leaf_certificate
|
78
|
+
certificate_chain[0]
|
79
|
+
end
|
80
|
+
|
81
|
+
def headers
|
82
|
+
jws_parts[HEADERS_POSITION]
|
83
|
+
end
|
84
|
+
|
85
|
+
def payload
|
86
|
+
jws_parts[PAYLOAD_POSITION]
|
87
|
+
end
|
88
|
+
|
89
|
+
def jws_parts
|
90
|
+
@jws_parts ||= JWT.decode(response, nil, false)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/cose/algorithm.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "cose/key"
|
4
|
+
|
3
5
|
# TODO: Move this to cose gem
|
4
6
|
module COSE
|
5
7
|
# https://tools.ietf.org/html/rfc8152#section-8.1
|
6
|
-
Algorithm = Struct.new(:id, :name, :hash, :key_curve) do
|
8
|
+
Algorithm = Struct.new(:id, :name, :hash, :kty, :key_curve) do
|
7
9
|
@registered = {}
|
8
10
|
|
9
|
-
def self.register(id, name, hash, key_curve)
|
10
|
-
@registered[id] = COSE::Algorithm.new(id, name, hash, key_curve)
|
11
|
+
def self.register(id, name, hash, kty, key_curve = nil)
|
12
|
+
@registered[id] = COSE::Algorithm.new(id, name, hash, kty, key_curve)
|
11
13
|
end
|
12
14
|
|
13
15
|
def self.find(id)
|
@@ -24,4 +26,5 @@ module COSE
|
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
27
|
-
COSE::Algorithm.register(-7, "ES256", "SHA256", "prime256v1")
|
29
|
+
COSE::Algorithm.register(-7, "ES256", "SHA256", COSE::Key::EC2::KTY_EC2, "prime256v1")
|
30
|
+
COSE::Algorithm.register(-257, "RS256", "SHA256", COSE::Key::RSA::KTY_RSA)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TPM
|
4
|
+
# Section 6 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
|
5
|
+
|
6
|
+
GENERATED_VALUE = 0xFF544347
|
7
|
+
|
8
|
+
ST_ATTEST_CERTIFY = 0x8017
|
9
|
+
|
10
|
+
# Algorithms
|
11
|
+
ALG_RSA = 0x0001
|
12
|
+
ALG_SHA1 = 0x0004
|
13
|
+
ALG_SHA256 = 0x000B
|
14
|
+
ALG_NULL = 0x0010
|
15
|
+
ALG_RSASSA = 0x0014
|
16
|
+
ALG_ECDSA = 0x0018
|
17
|
+
ALG_ECC = 0x0023
|
18
|
+
|
19
|
+
# ECC curves
|
20
|
+
ECC_NIST_P256 = 0x0003
|
21
|
+
end
|
data/lib/tpm/s_attest.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bindata"
|
4
|
+
require "tpm/constants"
|
5
|
+
require "tpm/sized_buffer"
|
6
|
+
require "tpm/s_attest/s_certify_info"
|
7
|
+
|
8
|
+
module TPM
|
9
|
+
# Section 10.12.8 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
|
10
|
+
class SAttest < BinData::Record
|
11
|
+
endian :big
|
12
|
+
|
13
|
+
uint32 :magic
|
14
|
+
uint16 :attested_type
|
15
|
+
sized_buffer :qualified_signer
|
16
|
+
sized_buffer :extra_data
|
17
|
+
|
18
|
+
# s_clock_info :clock_info
|
19
|
+
# uint64 :firmware_version
|
20
|
+
skip length: 25
|
21
|
+
|
22
|
+
choice :attested, selection: :attested_type do
|
23
|
+
s_certify_info TPM::ST_ATTEST_CERTIFY
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bindata"
|
4
|
+
require "tpm/sized_buffer"
|
5
|
+
|
6
|
+
module TPM
|
7
|
+
class SAttest < BinData::Record
|
8
|
+
# Section 10.12.3 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
|
9
|
+
class SCertifyInfo < BinData::Record
|
10
|
+
sized_buffer :name
|
11
|
+
sized_buffer :qualified_name
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bindata"
|
4
|
+
|
5
|
+
module TPM
|
6
|
+
# Section 10.4 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
|
7
|
+
class SizedBuffer < BinData::Record
|
8
|
+
endian :big
|
9
|
+
|
10
|
+
uint16 :buffer_size, value: lambda { buffer.size }
|
11
|
+
string :buffer, read_length: :buffer_size
|
12
|
+
end
|
13
|
+
end
|
data/lib/tpm/t_public.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bindata"
|
4
|
+
require "tpm/constants"
|
5
|
+
require "tpm/sized_buffer"
|
6
|
+
require "tpm/t_public/s_ecc_parms"
|
7
|
+
require "tpm/t_public/s_rsa_parms"
|
8
|
+
|
9
|
+
module TPM
|
10
|
+
# Section 12.2.4 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
|
11
|
+
class TPublic < BinData::Record
|
12
|
+
endian :big
|
13
|
+
|
14
|
+
uint16 :alg_type
|
15
|
+
uint16 :name_alg
|
16
|
+
|
17
|
+
# :object_attributes
|
18
|
+
skip length: 4
|
19
|
+
|
20
|
+
sized_buffer :auth_policy
|
21
|
+
|
22
|
+
choice :parameters, selection: :alg_type do
|
23
|
+
s_ecc_parms TPM::ALG_ECC
|
24
|
+
s_rsa_parms TPM::ALG_RSA
|
25
|
+
end
|
26
|
+
|
27
|
+
choice :unique, selection: :alg_type do
|
28
|
+
sized_buffer TPM::ALG_ECC
|
29
|
+
sized_buffer TPM::ALG_RSA
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bindata"
|
4
|
+
|
5
|
+
module TPM
|
6
|
+
class TPublic < BinData::Record
|
7
|
+
# Section 12.2.3.6 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
|
8
|
+
class SEccParms < BinData::Record
|
9
|
+
endian :big
|
10
|
+
|
11
|
+
uint16 :symmetric
|
12
|
+
uint16 :scheme
|
13
|
+
uint16 :curve_id
|
14
|
+
uint16 :kdf
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bindata"
|
4
|
+
|
5
|
+
module TPM
|
6
|
+
class TPublic < BinData::Record
|
7
|
+
# Section 12.2.3.5 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
|
8
|
+
class SRsaParms < BinData::Record
|
9
|
+
endian :big
|
10
|
+
|
11
|
+
uint16 :symmetric
|
12
|
+
uint16 :scheme
|
13
|
+
uint16 :key_bits
|
14
|
+
uint32 :exponent
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/webauthn.rb
CHANGED
@@ -11,7 +11,12 @@ require "securerandom"
|
|
11
11
|
require "json"
|
12
12
|
|
13
13
|
module WebAuthn
|
14
|
-
|
14
|
+
DEFAULT_ALGORITHMS = ["ES256", "RS256"].freeze
|
15
|
+
|
16
|
+
DEFAULT_PUB_KEY_CRED_PARAMS = DEFAULT_ALGORITHMS.map do |alg_name|
|
17
|
+
{ type: "public-key", alg: COSE::Algorithm.by_name(alg_name).id }
|
18
|
+
end.freeze
|
19
|
+
|
15
20
|
TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze
|
16
21
|
|
17
22
|
# TODO: make keyword arguments mandatory in next major version
|
@@ -23,7 +28,7 @@ module WebAuthn
|
|
23
28
|
)
|
24
29
|
{
|
25
30
|
challenge: challenge,
|
26
|
-
pubKeyCredParams:
|
31
|
+
pubKeyCredParams: DEFAULT_PUB_KEY_CRED_PARAMS,
|
27
32
|
rp: { name: rp_name },
|
28
33
|
user: { name: user_name, displayName: display_name, id: user_id }
|
29
34
|
}
|
@@ -11,6 +11,7 @@ module WebAuthn
|
|
11
11
|
ATTESTATION_FORMAT_PACKED = 'packed'
|
12
12
|
ATTESTATION_FORMAT_ANDROID_SAFETYNET = "android-safetynet"
|
13
13
|
ATTESTATION_FORMAT_ANDROID_KEY = "android-key"
|
14
|
+
ATTESTATION_FORMAT_TPM = "tpm"
|
14
15
|
|
15
16
|
ATTESTATION_TYPE_NONE = "None"
|
16
17
|
ATTESTATION_TYPE_BASIC = "Basic"
|
@@ -36,6 +37,9 @@ module WebAuthn
|
|
36
37
|
when ATTESTATION_FORMAT_ANDROID_KEY
|
37
38
|
require "webauthn/attestation_statement/android_key"
|
38
39
|
WebAuthn::AttestationStatement::AndroidKey.new(statement)
|
40
|
+
when ATTESTATION_FORMAT_TPM
|
41
|
+
require "webauthn/attestation_statement/tpm"
|
42
|
+
WebAuthn::AttestationStatement::TPM.new(statement)
|
39
43
|
else
|
40
44
|
raise FormatNotSupportedError, "Unsupported attestation format '#{format}'"
|
41
45
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "cose/algorithm"
|
4
3
|
require "openssl"
|
5
4
|
require "webauthn/attestation_statement/android_key/key_description"
|
6
5
|
require "webauthn/attestation_statement/base"
|
6
|
+
require "webauthn/signature_verifier"
|
7
7
|
|
8
8
|
module WebAuthn
|
9
9
|
module AttestationStatement
|
@@ -27,17 +27,9 @@ module WebAuthn
|
|
27
27
|
private
|
28
28
|
|
29
29
|
def valid_signature?(authenticator_data, client_data_hash)
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
attestation_certificate.public_key.verify(
|
34
|
-
cose_algorithm.hash,
|
35
|
-
signature,
|
36
|
-
authenticator_data.data + client_data_hash
|
37
|
-
)
|
38
|
-
else
|
39
|
-
raise "Unsupported algorithm #{algorithm}"
|
40
|
-
end
|
30
|
+
WebAuthn::SignatureVerifier
|
31
|
+
.new(algorithm, attestation_certificate.public_key)
|
32
|
+
.verify(signature, authenticator_data.data + client_data_hash)
|
41
33
|
end
|
42
34
|
|
43
35
|
def matching_public_key?(authenticator_data)
|
@@ -76,28 +68,6 @@ module WebAuthn
|
|
76
68
|
KeyDescription.new(OpenSSL::ASN1.decode(raw_key_description.value).value)
|
77
69
|
end
|
78
70
|
end
|
79
|
-
|
80
|
-
def attestation_certificate
|
81
|
-
attestation_certificate_chain[0]
|
82
|
-
end
|
83
|
-
|
84
|
-
def attestation_certificate_chain
|
85
|
-
@attestation_certificate_chain ||= raw_attestation_certificates.map do |cert|
|
86
|
-
OpenSSL::X509::Certificate.new(cert)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def raw_attestation_certificates
|
91
|
-
statement["x5c"]
|
92
|
-
end
|
93
|
-
|
94
|
-
def signature
|
95
|
-
statement["sig"]
|
96
|
-
end
|
97
|
-
|
98
|
-
def algorithm
|
99
|
-
statement["alg"]
|
100
|
-
end
|
101
71
|
end
|
102
72
|
end
|
103
73
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "android_safetynet/attestation_response"
|
4
4
|
require "openssl"
|
5
5
|
require "webauthn/attestation_statement/base"
|
6
6
|
|
@@ -14,10 +14,8 @@ module WebAuthn
|
|
14
14
|
|
15
15
|
def valid?(authenticator_data, client_data_hash, trust_store: self.class.default_trust_store)
|
16
16
|
trusted_attestation_certificate?(trust_store) &&
|
17
|
-
|
18
|
-
valid_attestation_domain? &&
|
17
|
+
valid_response?(authenticator_data, client_data_hash) &&
|
19
18
|
valid_version? &&
|
20
|
-
valid_nonce?(authenticator_data, client_data_hash) &&
|
21
19
|
cts_profile_match? &&
|
22
20
|
[WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_certificate]
|
23
21
|
end
|
@@ -31,15 +29,14 @@ module WebAuthn
|
|
31
29
|
trust_store.verify(attestation_certificate)
|
32
30
|
end
|
33
31
|
|
34
|
-
def
|
35
|
-
|
36
|
-
signature = Base64.urlsafe_decode64(base64_signature)
|
37
|
-
attestation_certificate.public_key.verify(OpenSSL::Digest::SHA256.new, signature, signed_payload)
|
38
|
-
end
|
32
|
+
def valid_response?(authenticator_data, client_data_hash)
|
33
|
+
nonce = Digest::SHA256.base64digest(authenticator_data.data + client_data_hash)
|
39
34
|
|
40
|
-
|
41
|
-
|
42
|
-
|
35
|
+
begin
|
36
|
+
attestation_response.verify(nonce)
|
37
|
+
rescue ::AndroidSafetynet::AttestationResponse::VerificationError
|
38
|
+
false
|
39
|
+
end
|
43
40
|
end
|
44
41
|
|
45
42
|
# TODO: improve once the spec has clarifications https://github.com/w3c/webauthn/issues/968
|
@@ -47,35 +44,20 @@ module WebAuthn
|
|
47
44
|
!statement["ver"].empty?
|
48
45
|
end
|
49
46
|
|
50
|
-
def valid_nonce?(authenticator_data, client_data_hash)
|
51
|
-
nonce = unverified_jws_result[0]["nonce"]
|
52
|
-
nonce == verification_data(authenticator_data, client_data_hash)
|
53
|
-
end
|
54
|
-
|
55
47
|
def cts_profile_match?
|
56
|
-
|
57
|
-
end
|
58
|
-
|
59
|
-
def verification_data(authenticator_data, client_data_hash)
|
60
|
-
Digest::SHA256.base64digest(authenticator_data.data + client_data_hash)
|
48
|
+
attestation_response.cts_profile_match?
|
61
49
|
end
|
62
50
|
|
63
51
|
def attestation_certificate
|
64
|
-
|
52
|
+
attestation_response.certificate_chain[0]
|
65
53
|
end
|
66
54
|
|
67
55
|
def signing_certificates
|
68
|
-
|
69
|
-
end
|
70
|
-
|
71
|
-
def attestation_certificate_chain
|
72
|
-
@attestation_certificate_chain ||= unverified_jws_result[1]["x5c"].map do |cert|
|
73
|
-
OpenSSL::X509::Certificate.new(Base64.strict_decode64(cert))
|
74
|
-
end
|
56
|
+
attestation_response.certificate_chain[1..-1]
|
75
57
|
end
|
76
58
|
|
77
|
-
def
|
78
|
-
@
|
59
|
+
def attestation_response
|
60
|
+
@attestation_response ||= ::AndroidSafetynet::AttestationResponse.new(statement["response"])
|
79
61
|
end
|
80
62
|
end
|
81
63
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "openssl"
|
3
4
|
require "webauthn/error"
|
4
5
|
|
5
6
|
module WebAuthn
|
@@ -20,6 +21,44 @@ module WebAuthn
|
|
20
21
|
private
|
21
22
|
|
22
23
|
attr_reader :statement
|
24
|
+
|
25
|
+
def matching_aaguid?(attested_credential_data_aaguid)
|
26
|
+
extension = attestation_certificate&.extensions&.detect { |ext| ext.oid == AAGUID_EXTENSION_OID }
|
27
|
+
if extension
|
28
|
+
# `extension.value` mangles data into ASCII, so we must manually compare bytes
|
29
|
+
# see https://github.com/ruby/openssl/pull/234
|
30
|
+
extension.to_der[-WebAuthn::AuthenticatorData::AttestedCredentialData::AAGUID_LENGTH..-1] ==
|
31
|
+
attested_credential_data_aaguid
|
32
|
+
else
|
33
|
+
true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def attestation_certificate
|
38
|
+
attestation_certificate_chain&.first
|
39
|
+
end
|
40
|
+
|
41
|
+
def attestation_certificate_chain
|
42
|
+
@attestation_certificate_chain ||= raw_attestation_certificates&.map do |raw_certificate|
|
43
|
+
OpenSSL::X509::Certificate.new(raw_certificate)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def algorithm
|
48
|
+
statement["alg"]
|
49
|
+
end
|
50
|
+
|
51
|
+
def raw_attestation_certificates
|
52
|
+
statement["x5c"]
|
53
|
+
end
|
54
|
+
|
55
|
+
def raw_ecdaa_key_id
|
56
|
+
statement["ecdaaKeyId"]
|
57
|
+
end
|
58
|
+
|
59
|
+
def signature
|
60
|
+
statement["sig"]
|
61
|
+
end
|
23
62
|
end
|
24
63
|
end
|
25
64
|
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "openssl"
|
4
4
|
require "webauthn/attestation_statement/base"
|
5
5
|
require "webauthn/attestation_statement/fido_u2f/public_key"
|
6
|
+
require "webauthn/signature_verifier"
|
6
7
|
|
7
8
|
module WebAuthn
|
8
9
|
module AttestationStatement
|
@@ -22,10 +23,6 @@ module WebAuthn
|
|
22
23
|
|
23
24
|
private
|
24
25
|
|
25
|
-
def signature
|
26
|
-
statement["sig"]
|
27
|
-
end
|
28
|
-
|
29
26
|
def valid_format?
|
30
27
|
!!(raw_attestation_certificates && signature) &&
|
31
28
|
raw_attestation_certificates.length == VALID_ATTESTATION_CERTIFICATE_COUNT
|
@@ -45,24 +42,14 @@ module WebAuthn
|
|
45
42
|
attestation_certificate.public_key
|
46
43
|
end
|
47
44
|
|
48
|
-
def attestation_certificate
|
49
|
-
@attestation_certificate ||= OpenSSL::X509::Certificate.new(raw_attestation_certificates[0])
|
50
|
-
end
|
51
|
-
|
52
|
-
def raw_attestation_certificates
|
53
|
-
statement["x5c"]
|
54
|
-
end
|
55
|
-
|
56
45
|
def valid_aaguid?(attested_credential_data_aaguid)
|
57
46
|
attested_credential_data_aaguid == VALID_ATTESTED_AAGUID
|
58
47
|
end
|
59
48
|
|
60
49
|
def valid_signature?(authenticator_data, client_data_hash)
|
61
|
-
|
62
|
-
VALID_ATTESTATION_CERTIFICATE_ALGORITHM
|
63
|
-
signature,
|
64
|
-
verification_data(authenticator_data, client_data_hash)
|
65
|
-
)
|
50
|
+
WebAuthn::SignatureVerifier
|
51
|
+
.new(VALID_ATTESTATION_CERTIFICATE_ALGORITHM, certificate_public_key)
|
52
|
+
.verify(signature, verification_data(authenticator_data, client_data_hash))
|
66
53
|
end
|
67
54
|
|
68
55
|
def verification_data(authenticator_data, client_data_hash)
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "cose/algorithm"
|
4
3
|
require "openssl"
|
5
4
|
require "webauthn/attestation_statement/base"
|
5
|
+
require "webauthn/signature_verifier"
|
6
6
|
|
7
7
|
module WebAuthn
|
8
8
|
# Implements https://www.w3.org/TR/2018/CR-webauthn-20180807/#packed-attestation
|
@@ -16,7 +16,7 @@ module WebAuthn
|
|
16
16
|
valid_format? &&
|
17
17
|
valid_algorithm?(authenticator_data.credential) &&
|
18
18
|
valid_certificate_chain? &&
|
19
|
-
|
19
|
+
valid_ec_public_keys?(authenticator_data.credential) &&
|
20
20
|
meet_certificate_requirement? &&
|
21
21
|
matching_aaguid?(authenticator_data.attested_credential_data.aaguid) &&
|
22
22
|
valid_signature?(authenticator_data, client_data_hash) &&
|
@@ -33,22 +33,6 @@ module WebAuthn
|
|
33
33
|
!raw_attestation_certificates && !raw_ecdaa_key_id
|
34
34
|
end
|
35
35
|
|
36
|
-
def algorithm
|
37
|
-
statement["alg"]
|
38
|
-
end
|
39
|
-
|
40
|
-
def signature
|
41
|
-
statement["sig"]
|
42
|
-
end
|
43
|
-
|
44
|
-
def raw_attestation_certificates
|
45
|
-
statement["x5c"]
|
46
|
-
end
|
47
|
-
|
48
|
-
def raw_ecdaa_key_id
|
49
|
-
statement["ecdaaKeyId"]
|
50
|
-
end
|
51
|
-
|
52
36
|
def valid_format?
|
53
37
|
algorithm && signature && (
|
54
38
|
[raw_attestation_certificates, raw_ecdaa_key_id].compact.size < 2
|
@@ -61,16 +45,6 @@ module WebAuthn
|
|
61
45
|
end
|
62
46
|
end
|
63
47
|
|
64
|
-
def attestation_certificate_chain
|
65
|
-
@attestation_certificate_chain ||= raw_attestation_certificates&.map do |cert|
|
66
|
-
OpenSSL::X509::Certificate.new(cert)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def attestation_certificate
|
71
|
-
attestation_certificate_chain&.first
|
72
|
-
end
|
73
|
-
|
74
48
|
def valid_certificate_chain?
|
75
49
|
if attestation_certificate_chain
|
76
50
|
attestation_certificate_chain[1..-1].all? { |c| certificate_in_use?(c) }
|
@@ -79,12 +53,10 @@ module WebAuthn
|
|
79
53
|
end
|
80
54
|
end
|
81
55
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
public_key.is_a?(OpenSSL::PKey::EC) && public_key.check_key
|
87
|
-
end
|
56
|
+
def valid_ec_public_keys?(credential)
|
57
|
+
(attestation_certificate_chain&.map(&:public_key) || [credential.public_key_object])
|
58
|
+
.select { |pkey| pkey.is_a?(OpenSSL::PKey::EC) }
|
59
|
+
.all? { |pkey| pkey.check_key }
|
88
60
|
end
|
89
61
|
|
90
62
|
# Check https://www.w3.org/TR/2018/CR-webauthn-20180807/#packed-attestation-cert-requirements
|
@@ -101,18 +73,6 @@ module WebAuthn
|
|
101
73
|
end
|
102
74
|
end
|
103
75
|
|
104
|
-
def matching_aaguid?(attested_credential_data_aaguid)
|
105
|
-
extension = attestation_certificate&.extensions&.detect { |ext| ext.oid == AAGUID_EXTENSION_OID }
|
106
|
-
if extension
|
107
|
-
# `extension.value` mangles data into ASCII, so we must manually compare bytes
|
108
|
-
# see https://github.com/ruby/openssl/pull/234
|
109
|
-
extension.to_der[-WebAuthn::AuthenticatorData::AttestedCredentialData::AAGUID_LENGTH..-1] ==
|
110
|
-
attested_credential_data_aaguid
|
111
|
-
else
|
112
|
-
true
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
76
|
def certificate_in_use?(certificate)
|
117
77
|
now = Time.now
|
118
78
|
|
@@ -120,21 +80,12 @@ module WebAuthn
|
|
120
80
|
end
|
121
81
|
|
122
82
|
def valid_signature?(authenticator_data, client_data_hash)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
cose_algorithm.hash,
|
128
|
-
signature,
|
129
|
-
verification_data(authenticator_data, client_data_hash)
|
130
|
-
)
|
131
|
-
else
|
132
|
-
raise "Unsupported algorithm #{algorithm}"
|
133
|
-
end
|
134
|
-
end
|
83
|
+
signature_verifier = WebAuthn::SignatureVerifier.new(
|
84
|
+
algorithm,
|
85
|
+
attestation_certificate&.public_key || authenticator_data.credential.public_key_object
|
86
|
+
)
|
135
87
|
|
136
|
-
|
137
|
-
authenticator_data.data + client_data_hash
|
88
|
+
signature_verifier.verify(signature, authenticator_data.data + client_data_hash)
|
138
89
|
end
|
139
90
|
|
140
91
|
def attestation_type_and_trust_path
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cose/algorithm"
|
4
|
+
require "openssl"
|
5
|
+
require "webauthn/attestation_statement/base"
|
6
|
+
require "webauthn/attestation_statement/tpm/cert_info"
|
7
|
+
require "webauthn/attestation_statement/tpm/pub_area"
|
8
|
+
require "webauthn/signature_verifier"
|
9
|
+
|
10
|
+
module WebAuthn
|
11
|
+
module AttestationStatement
|
12
|
+
class TPM < Base
|
13
|
+
CERTIFICATE_V3 = 2
|
14
|
+
CERTIFICATE_EMPTY_NAME = OpenSSL::X509::Name.new([]).freeze
|
15
|
+
OID_TCG_KP_AIK_CERTIFICATE = "2.23.133.8.3"
|
16
|
+
TPM_V2 = "2.0"
|
17
|
+
|
18
|
+
def valid?(authenticator_data, client_data_hash)
|
19
|
+
case attestation_type
|
20
|
+
when ATTESTATION_TYPE_ATTCA
|
21
|
+
att_to_be_signed = authenticator_data.data + client_data_hash
|
22
|
+
|
23
|
+
ver == TPM_V2 &&
|
24
|
+
valid_signature? &&
|
25
|
+
valid_attestation_certificate? &&
|
26
|
+
pub_area.valid?(authenticator_data.credential.public_key) &&
|
27
|
+
cert_info.valid?(statement["pubArea"], OpenSSL::Digest.digest(cose_algorithm.hash, att_to_be_signed)) &&
|
28
|
+
matching_aaguid?(authenticator_data.attested_credential_data.aaguid) &&
|
29
|
+
[attestation_type, attestation_trust_path]
|
30
|
+
when ATTESTATION_TYPE_ECDAA
|
31
|
+
raise(
|
32
|
+
WebAuthn::AttestationStatement::Base::NotSupportedError,
|
33
|
+
"Attestation type ECDAA is not supported"
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def valid_signature?
|
41
|
+
WebAuthn::SignatureVerifier
|
42
|
+
.new(algorithm, attestation_certificate.public_key)
|
43
|
+
.verify(signature, verification_data)
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid_attestation_certificate?
|
47
|
+
extensions = attestation_certificate.extensions
|
48
|
+
|
49
|
+
attestation_certificate.version == CERTIFICATE_V3 &&
|
50
|
+
attestation_certificate.subject.eql?(CERTIFICATE_EMPTY_NAME) &&
|
51
|
+
certificate_in_use?(attestation_certificate) &&
|
52
|
+
extensions.find { |ext| ext.oid == 'basicConstraints' }&.value == "CA:FALSE" &&
|
53
|
+
extensions.find { |ext| ext.oid == "extendedKeyUsage" }&.value == OID_TCG_KP_AIK_CERTIFICATE
|
54
|
+
end
|
55
|
+
|
56
|
+
def certificate_in_use?(certificate)
|
57
|
+
now = Time.now
|
58
|
+
|
59
|
+
certificate.not_before < now && now < certificate.not_after
|
60
|
+
end
|
61
|
+
|
62
|
+
def verification_data
|
63
|
+
statement["certInfo"]
|
64
|
+
end
|
65
|
+
|
66
|
+
def cert_info
|
67
|
+
@cert_info ||= CertInfo.new(statement["certInfo"])
|
68
|
+
end
|
69
|
+
|
70
|
+
def pub_area
|
71
|
+
@pub_area ||= PubArea.new(statement["pubArea"])
|
72
|
+
end
|
73
|
+
|
74
|
+
def ver
|
75
|
+
statement["ver"]
|
76
|
+
end
|
77
|
+
|
78
|
+
def cose_algorithm
|
79
|
+
@cose_algorithm ||= COSE::Algorithm.find(algorithm)
|
80
|
+
end
|
81
|
+
|
82
|
+
def attestation_type
|
83
|
+
if raw_attestation_certificates && !raw_ecdaa_key_id
|
84
|
+
ATTESTATION_TYPE_ATTCA
|
85
|
+
elsif raw_ecdaa_key_id && !raw_attestation_certificates
|
86
|
+
ATTESTATION_TYPE_ECDAA
|
87
|
+
else
|
88
|
+
raise "Attestation type invalid"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def attestation_trust_path
|
93
|
+
attestation_certificate_chain
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tpm/constants"
|
4
|
+
require "tpm/s_attest"
|
5
|
+
|
6
|
+
module WebAuthn
|
7
|
+
module AttestationStatement
|
8
|
+
class TPM < Base
|
9
|
+
class CertInfo
|
10
|
+
TPM_TO_OPENSSL_HASH_ALG = {
|
11
|
+
::TPM::ALG_SHA1 => "SHA1",
|
12
|
+
::TPM::ALG_SHA256 => "SHA256"
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize(data)
|
16
|
+
@data = data
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?(attested_data, extra_data)
|
20
|
+
s_attest.magic == ::TPM::GENERATED_VALUE &&
|
21
|
+
valid_name?(attested_data) &&
|
22
|
+
s_attest.extra_data.buffer == extra_data
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :data
|
28
|
+
|
29
|
+
def valid_name?(attested_data)
|
30
|
+
name_hash_alg = s_attest.attested.name.buffer[0..1].unpack("n")[0]
|
31
|
+
name = s_attest.attested.name.buffer[2..-1]
|
32
|
+
|
33
|
+
name == OpenSSL::Digest.digest(TPM_TO_OPENSSL_HASH_ALG[name_hash_alg], attested_data)
|
34
|
+
end
|
35
|
+
|
36
|
+
def s_attest
|
37
|
+
@s_attest ||= ::TPM::SAttest.read(data)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cose/key"
|
4
|
+
require "tpm/constants"
|
5
|
+
require "tpm/t_public"
|
6
|
+
|
7
|
+
module WebAuthn
|
8
|
+
module AttestationStatement
|
9
|
+
class TPM < Base
|
10
|
+
class PubArea
|
11
|
+
BYTE_LENGTH = 8
|
12
|
+
|
13
|
+
COSE_ECC_TO_TPM_ALG = {
|
14
|
+
COSE::Algorithm.by_name("ES256").id => ::TPM::ALG_ECDSA,
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
COSE_RSA_TO_TPM_ALG = {
|
18
|
+
COSE::Algorithm.by_name("RS256").id => ::TPM::ALG_RSASSA
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
COSE_TO_TPM_CURVE = {
|
22
|
+
COSE::Key::EC2::CRV_P256 => ::TPM::ECC_NIST_P256
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
def initialize(data)
|
26
|
+
@data = data
|
27
|
+
end
|
28
|
+
|
29
|
+
def valid?(public_key)
|
30
|
+
cose_key = COSE::Key.deserialize(public_key)
|
31
|
+
|
32
|
+
case cose_key
|
33
|
+
when COSE::Key::EC2
|
34
|
+
valid_ecc_key?(cose_key)
|
35
|
+
when COSE::Key::RSA
|
36
|
+
valid_rsa_key?(cose_key)
|
37
|
+
else
|
38
|
+
raise "Unsupported or unknown TPM key type"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :data
|
45
|
+
|
46
|
+
def valid_ecc_key?(cose_key)
|
47
|
+
valid_symmetric? &&
|
48
|
+
valid_scheme?(COSE_ECC_TO_TPM_ALG[cose_key.alg]) &&
|
49
|
+
parameters.curve_id == COSE_TO_TPM_CURVE[cose_key.crv] &&
|
50
|
+
unique == cose_key.x + cose_key.y
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid_rsa_key?(cose_key)
|
54
|
+
valid_symmetric? &&
|
55
|
+
valid_scheme?(COSE_RSA_TO_TPM_ALG[cose_key.alg]) &&
|
56
|
+
parameters.key_bits == cose_key.n.size * BYTE_LENGTH &&
|
57
|
+
unique == cose_key.n
|
58
|
+
end
|
59
|
+
|
60
|
+
def valid_symmetric?
|
61
|
+
parameters.symmetric == ::TPM::ALG_NULL
|
62
|
+
end
|
63
|
+
|
64
|
+
def valid_scheme?(scheme)
|
65
|
+
parameters.scheme == ::TPM::ALG_NULL || parameters.scheme == scheme
|
66
|
+
end
|
67
|
+
|
68
|
+
def unique
|
69
|
+
t_public.unique.buffer
|
70
|
+
end
|
71
|
+
|
72
|
+
def parameters
|
73
|
+
t_public.parameters
|
74
|
+
end
|
75
|
+
|
76
|
+
def t_public
|
77
|
+
@t_public = ::TPM::TPublic.read(data)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -4,6 +4,7 @@ require "cose/algorithm"
|
|
4
4
|
require "cose/key"
|
5
5
|
require "webauthn/attestation_statement/fido_u2f/public_key"
|
6
6
|
require "webauthn/authenticator_response"
|
7
|
+
require "webauthn/signature_verifier"
|
7
8
|
|
8
9
|
module WebAuthn
|
9
10
|
class CredentialVerificationError < VerificationError; end
|
@@ -18,8 +19,8 @@ module WebAuthn
|
|
18
19
|
@signature = signature
|
19
20
|
end
|
20
21
|
|
21
|
-
def verify(
|
22
|
-
super(
|
22
|
+
def verify(expected_challenge, expected_origin, allowed_credentials:, rp_id: nil)
|
23
|
+
super(expected_challenge, expected_origin, rp_id: rp_id)
|
23
24
|
|
24
25
|
verify_item(:credential, allowed_credentials)
|
25
26
|
verify_item(:signature, credential_cose_key(allowed_credentials))
|
@@ -36,17 +37,9 @@ module WebAuthn
|
|
36
37
|
attr_reader :credential_id, :authenticator_data_bytes, :signature
|
37
38
|
|
38
39
|
def valid_signature?(credential_cose_key)
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
credential_cose_key.to_pkey.verify(
|
43
|
-
cose_algorithm.hash,
|
44
|
-
signature,
|
45
|
-
authenticator_data_bytes + client_data.hash
|
46
|
-
)
|
47
|
-
else
|
48
|
-
raise "Unsupported algorithm #{credential_cose_key.alg}"
|
49
|
-
end
|
40
|
+
WebAuthn::SignatureVerifier
|
41
|
+
.new(credential_cose_key.alg, credential_cose_key.to_pkey)
|
42
|
+
.verify(signature, authenticator_data_bytes + client_data.hash)
|
50
43
|
end
|
51
44
|
|
52
45
|
def valid_credential?(allowed_credentials)
|
@@ -18,13 +18,13 @@ module WebAuthn
|
|
18
18
|
@client_data_json = client_data_json
|
19
19
|
end
|
20
20
|
|
21
|
-
def verify(
|
21
|
+
def verify(expected_challenge, expected_origin, rp_id: nil)
|
22
22
|
verify_item(:type)
|
23
23
|
verify_item(:token_binding)
|
24
|
-
verify_item(:challenge,
|
25
|
-
verify_item(:origin,
|
24
|
+
verify_item(:challenge, expected_challenge)
|
25
|
+
verify_item(:origin, expected_origin)
|
26
26
|
verify_item(:authenticator_data)
|
27
|
-
verify_item(:rp_id, rp_id || rp_id_from_origin(
|
27
|
+
verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin))
|
28
28
|
verify_item(:user_presence)
|
29
29
|
|
30
30
|
true
|
@@ -62,12 +62,12 @@ module WebAuthn
|
|
62
62
|
client_data.valid_token_binding_format?
|
63
63
|
end
|
64
64
|
|
65
|
-
def valid_challenge?(
|
66
|
-
WebAuthn::SecurityUtils.secure_compare(Base64.urlsafe_decode64(client_data.challenge),
|
65
|
+
def valid_challenge?(expected_challenge)
|
66
|
+
WebAuthn::SecurityUtils.secure_compare(Base64.urlsafe_decode64(client_data.challenge), expected_challenge)
|
67
67
|
end
|
68
68
|
|
69
|
-
def valid_origin?(
|
70
|
-
client_data.origin ==
|
69
|
+
def valid_origin?(expected_origin)
|
70
|
+
client_data.origin == expected_origin
|
71
71
|
end
|
72
72
|
|
73
73
|
def valid_rp_id?(rp_id)
|
@@ -82,8 +82,8 @@ module WebAuthn
|
|
82
82
|
authenticator_data.user_flagged?
|
83
83
|
end
|
84
84
|
|
85
|
-
def rp_id_from_origin(
|
86
|
-
URI.parse(
|
85
|
+
def rp_id_from_origin(expected_origin)
|
86
|
+
URI.parse(expected_origin).host
|
87
87
|
end
|
88
88
|
|
89
89
|
def type
|
@@ -93,16 +93,23 @@ module WebAuthn
|
|
93
93
|
end
|
94
94
|
|
95
95
|
def cose_credential_public_key
|
96
|
-
|
97
|
-
|
98
|
-
COSE::Key::
|
99
|
-
|
100
|
-
|
96
|
+
case credential[:public_key]
|
97
|
+
when OpenSSL::PKey::RSA
|
98
|
+
key = COSE::Key::RSA.from_pkey(credential[:public_key])
|
99
|
+
# FIXME: Remove once writer in cose
|
100
|
+
key.instance_variable_set(:@alg, -257)
|
101
|
+
when OpenSSL::PKey::EC::Point
|
102
|
+
alg = {
|
103
|
+
COSE::Key::EC2::CRV_P256 => -7,
|
104
|
+
COSE::Key::EC2::CRV_P384 => -35,
|
105
|
+
COSE::Key::EC2::CRV_P521 => -36
|
106
|
+
}
|
107
|
+
|
108
|
+
key = COSE::Key::EC2.from_pkey(credential[:public_key])
|
109
|
+
# FIXME: Remove once writer in cose
|
110
|
+
key.instance_variable_set(:@alg, alg[key.crv])
|
101
111
|
|
102
|
-
|
103
|
-
|
104
|
-
# FIXME: Remove once writer in cose
|
105
|
-
key.instance_variable_set(:@alg, alg[key.crv])
|
112
|
+
end
|
106
113
|
|
107
114
|
key.serialize
|
108
115
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cose/algorithm"
|
4
|
+
|
5
|
+
module WebAuthn
|
6
|
+
class SignatureVerifier
|
7
|
+
class UnsupportedAlgorithm < Error; end
|
8
|
+
|
9
|
+
# This logic contained in this map constant is a candidate to be moved to cose gem domain
|
10
|
+
KTY_MAP = {
|
11
|
+
COSE::Key::EC2::KTY_EC2 => [OpenSSL::PKey::EC, OpenSSL::PKey::EC::Point],
|
12
|
+
COSE::Key::RSA::KTY_RSA => [OpenSSL::PKey::RSA]
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize(algorithm, public_key)
|
16
|
+
@algorithm = algorithm
|
17
|
+
@public_key = public_key
|
18
|
+
|
19
|
+
validate
|
20
|
+
end
|
21
|
+
|
22
|
+
def verify(signature, verification_data)
|
23
|
+
public_key.verify(cose_algorithm.hash, signature, verification_data)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :algorithm, :public_key
|
29
|
+
|
30
|
+
def cose_algorithm
|
31
|
+
case algorithm
|
32
|
+
when COSE::Algorithm
|
33
|
+
algorithm
|
34
|
+
else
|
35
|
+
COSE::Algorithm.find(algorithm)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate
|
40
|
+
if cose_algorithm
|
41
|
+
if !KTY_MAP[cose_algorithm.kty].include?(public_key.class)
|
42
|
+
raise("Incompatible algorithm and key")
|
43
|
+
end
|
44
|
+
else
|
45
|
+
raise UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/webauthn/version.rb
CHANGED
data/webauthn.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: webauthn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gonzalo Rodriguez
|
@@ -9,8 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-04-
|
12
|
+
date: 2019-04-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bindata
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '2.4'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '2.4'
|
14
28
|
- !ruby/object:Gem::Dependency
|
15
29
|
name: cbor
|
16
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -200,7 +214,15 @@ files:
|
|
200
214
|
- bin/setup
|
201
215
|
- gemfiles/openssl_2_0.gemfile
|
202
216
|
- gemfiles/openssl_2_1.gemfile
|
217
|
+
- lib/android_safetynet/attestation_response.rb
|
203
218
|
- lib/cose/algorithm.rb
|
219
|
+
- lib/tpm/constants.rb
|
220
|
+
- lib/tpm/s_attest.rb
|
221
|
+
- lib/tpm/s_attest/s_certify_info.rb
|
222
|
+
- lib/tpm/sized_buffer.rb
|
223
|
+
- lib/tpm/t_public.rb
|
224
|
+
- lib/tpm/t_public/s_ecc_parms.rb
|
225
|
+
- lib/tpm/t_public/s_rsa_parms.rb
|
204
226
|
- lib/webauthn.rb
|
205
227
|
- lib/webauthn/attestation_statement.rb
|
206
228
|
- lib/webauthn/attestation_statement/android_key.rb
|
@@ -212,6 +234,9 @@ files:
|
|
212
234
|
- lib/webauthn/attestation_statement/fido_u2f/public_key.rb
|
213
235
|
- lib/webauthn/attestation_statement/none.rb
|
214
236
|
- lib/webauthn/attestation_statement/packed.rb
|
237
|
+
- lib/webauthn/attestation_statement/tpm.rb
|
238
|
+
- lib/webauthn/attestation_statement/tpm/cert_info.rb
|
239
|
+
- lib/webauthn/attestation_statement/tpm/pub_area.rb
|
215
240
|
- lib/webauthn/authenticator_assertion_response.rb
|
216
241
|
- lib/webauthn/authenticator_attestation_response.rb
|
217
242
|
- lib/webauthn/authenticator_data.rb
|
@@ -224,6 +249,7 @@ files:
|
|
224
249
|
- lib/webauthn/fake_authenticator/authenticator_data.rb
|
225
250
|
- lib/webauthn/fake_client.rb
|
226
251
|
- lib/webauthn/security_utils.rb
|
252
|
+
- lib/webauthn/signature_verifier.rb
|
227
253
|
- lib/webauthn/version.rb
|
228
254
|
- webauthn.gemspec
|
229
255
|
homepage: https://github.com/cedarcode/webauthn-ruby
|