webauthn 1.13.0 → 1.14.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/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
|