webauthn 1.15.0 → 1.16.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac8a0cc80530217e636ae8c83128363f9f9e725243cb40f5dad4e0941db4149f
4
- data.tar.gz: b781f8035cf6c25626b6da8e0ced92838d5d6f5defcecc7180f826af4184540d
3
+ metadata.gz: 42985c0e7686cf588be02d1afef8e315e0692561533f076e36419cd2ef3b2c74
4
+ data.tar.gz: 3970cd2424034f2a28e1ffb92954151aa1ae113391731f63f9d1734c10a61bce
5
5
  SHA512:
6
- metadata.gz: '01449706124d9e42b81c4691075e1de5f6192651a9cb8f798c2b78bb31ef0171d9ece8ef09412b73cbdb3444f38e963455439a5d1cf9eb781c8804e713dd5002'
7
- data.tar.gz: 4f5c370130707a529566f4be6887c592c600674caf5f2b198d9329de9b7de3098ec775cc222400d2ab8fe214310058b1b8563b769cc006089ddc1f2910526187
6
+ metadata.gz: 6993542bbc3ec899929f6ba5e13b34a347692f13da9efd7cab541de38e0473f2271c0c9e67085bd5e22439c59c97c7dc86680b7579cf578fd973eabc23fea2c4
7
+ data.tar.gz: c30c9f9e9c43fddf01621eff5f1068b9ecc280649801a53ba7daa736fb1eb88724efaf8e50e4d8768544ce8b2a725318693eec3b84c6d8cef0137d8d712f6e85
data/.travis.yml CHANGED
@@ -4,7 +4,8 @@ cache: bundler
4
4
 
5
5
  rvm:
6
6
  - ruby-head
7
- - 2.6.2
7
+ - 2.7.0-preview1
8
+ - 2.6.3
8
9
  - 2.5.5
9
10
  - 2.4.6
10
11
  - 2.3.8
@@ -17,6 +18,7 @@ matrix:
17
18
  fast_finish: true
18
19
  allow_failures:
19
20
  - rvm: ruby-head
21
+ - rvm: 2.7.0-preview1
20
22
 
21
23
  before_install:
22
24
  - wget http://archive.ubuntu.com/ubuntu/pool/universe/f/faketime/libfaketime_0.9.7-3_amd64.deb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.16.0] - 2019-06-13
4
+
5
+ ### Added
6
+
7
+ - Ability to enforce [user verification](https://www.w3.org/TR/webauthn/#user-verification) with extra argument in the `#verify` method.
8
+ - Support RS1 (RSA w/ SHA-1) credentials. Off by default. Enable by adding `"RS1"` to `WebAuthn.configuration.algorithms` array.
9
+ - Support PS256 (RSA Probabilistic Signature Scheme w/ SHA-256) credentials. On by default. Thank you @bdewater.
10
+
3
11
  ## [v1.15.0] - 2019-05-16
4
12
 
5
13
  ### Added
@@ -179,6 +187,7 @@ Note: Both additions should help making it compatible with Chrome for Android 70
179
187
  - `WebAuthn::AuthenticatorAttestationResponse.valid?` can be used to validate fido-u2f attestations returned by the browser
180
188
  - Works with ruby 2.5
181
189
 
190
+ [v1.16.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.15.0...v1.16.0/
182
191
  [v1.15.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.14.0...v1.15.0/
183
192
  [v1.14.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.13.0...v1.14.0/
184
193
  [v1.13.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.12.0...v1.13.0/
data/README.md CHANGED
@@ -23,6 +23,7 @@ WebAuthn (Web Authentication) is a W3C standard for secure public-key authentica
23
23
  - WebAuthn [article](https://en.wikipedia.org/wiki/WebAuthn) in Wikipedia
24
24
  - [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) in MDN
25
25
  - WebAuthn [article with talk](https://developers.google.com/web/updates/2018/05/webauthn) in Google Developers
26
+ - How to use [WebAuthn in Android apps](https://developers.google.com/identity/fido/android/native-apps)
26
27
 
27
28
  ## Prerequisites
28
29
 
@@ -27,4 +27,6 @@ module COSE
27
27
  end
28
28
 
29
29
  COSE::Algorithm.register(-7, "ES256", "SHA256", COSE::Key::EC2::KTY_EC2, "prime256v1")
30
+ COSE::Algorithm.register(-37, "PS256", "SHA256", COSE::Key::RSA::KTY_RSA)
30
31
  COSE::Algorithm.register(-257, "RS256", "SHA256", COSE::Key::RSA::KTY_RSA)
32
+ COSE::Algorithm.register(-65535, "RS1", "SHA1", COSE::Key::RSA::KTY_RSA)
@@ -20,8 +20,8 @@ module WebAuthn
20
20
  @signature = signature
21
21
  end
22
22
 
23
- def verify(expected_challenge, expected_origin = nil, allowed_credentials:, rp_id: nil)
24
- super(expected_challenge, expected_origin, rp_id: rp_id)
23
+ def verify(expected_challenge, expected_origin = nil, allowed_credentials:, user_verification: nil, rp_id: nil)
24
+ super(expected_challenge, expected_origin, user_verification: user_verification, rp_id: rp_id)
25
25
 
26
26
  verify_item(:credential, allowed_credentials)
27
27
  verify_item(:signature, credential_cose_key(allowed_credentials))
@@ -21,7 +21,7 @@ module WebAuthn
21
21
  @attestation_object = attestation_object
22
22
  end
23
23
 
24
- def verify(expected_challenge, expected_origin = nil, rp_id: nil)
24
+ def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil)
25
25
  super
26
26
 
27
27
  verify_item(:attestation_statement)
@@ -24,13 +24,9 @@ module WebAuthn
24
24
  attr_reader :data
25
25
 
26
26
  def valid?
27
- if attested_credential_data_included? || extension_data_included?
28
- data.length > base_length &&
29
- (!attested_credential_data_included? || attested_credential_data.valid?) &&
30
- (!extension_data_included? || extension_data)
31
- else
32
- data.length == base_length
33
- end
27
+ valid_length? &&
28
+ (!attested_credential_data_included? || attested_credential_data.valid?) &&
29
+ (!extension_data_included? || extension_data)
34
30
  end
35
31
 
36
32
  def user_flagged?
@@ -74,7 +70,7 @@ module WebAuthn
74
70
  end
75
71
 
76
72
  def extension_data
77
- @extension_data ||= CBOR.decode(data_at(extension_data_position))
73
+ @extension_data ||= CBOR.decode(raw_extension_data)
78
74
  end
79
75
 
80
76
  def flags
@@ -83,6 +79,14 @@ module WebAuthn
83
79
 
84
80
  private
85
81
 
82
+ def valid_length?
83
+ data.length == base_length + attested_credential_data_length + extension_data_length
84
+ end
85
+
86
+ def raw_extension_data
87
+ data_at(extension_data_position)
88
+ end
89
+
86
90
  def attested_credential_data_position
87
91
  base_length
88
92
  end
@@ -95,6 +99,14 @@ module WebAuthn
95
99
  end
96
100
  end
97
101
 
102
+ def extension_data_length
103
+ if extension_data_included?
104
+ raw_extension_data.length
105
+ else
106
+ 0
107
+ end
108
+ end
109
+
98
110
  def extension_data_position
99
111
  base_length + attested_credential_data_length
100
112
  end
@@ -17,13 +17,14 @@ module WebAuthn
17
17
  class TokenBindingVerificationError < VerificationError; end
18
18
  class TypeVerificationError < VerificationError; end
19
19
  class UserPresenceVerificationError < VerificationError; end
20
+ class UserVerifiedVerificationError < VerificationError; end
20
21
 
21
22
  class AuthenticatorResponse
22
23
  def initialize(client_data_json:)
23
24
  @client_data_json = client_data_json
24
25
  end
25
26
 
26
- def verify(expected_challenge, expected_origin = nil, rp_id: nil)
27
+ def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil)
27
28
  expected_origin ||= WebAuthn.configuration.origin || raise("Unspecified expected origin")
28
29
  rp_id ||= WebAuthn.configuration.rp_id
29
30
 
@@ -34,6 +35,7 @@ module WebAuthn
34
35
  verify_item(:authenticator_data)
35
36
  verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin))
36
37
  verify_item(:user_presence)
38
+ verify_item(:user_verified, user_verification)
37
39
 
38
40
  true
39
41
  end
@@ -90,6 +92,14 @@ module WebAuthn
90
92
  authenticator_data.user_flagged?
91
93
  end
92
94
 
95
+ def valid_user_verified?(user_verification)
96
+ if user_verification
97
+ authenticator_data.user_verified?
98
+ else
99
+ true
100
+ end
101
+ end
102
+
93
103
  def rp_id_from_origin(expected_origin)
94
104
  URI.parse(expected_origin).host
95
105
  end
@@ -10,8 +10,19 @@ module WebAuthn
10
10
  end
11
11
 
12
12
  class Configuration
13
+ def self.if_pss_supported(algorithm)
14
+ OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil
15
+ end
16
+
17
+ DEFAULT_ALGORITHMS = ["ES256", if_pss_supported("PS256"), "RS256"].compact.freeze
18
+
19
+ attr_accessor :algorithms
13
20
  attr_accessor :origin
14
21
  attr_accessor :rp_id
15
22
  attr_accessor :rp_name
23
+
24
+ def initialize
25
+ @algorithms = DEFAULT_ALGORITHMS.dup
26
+ end
16
27
  end
17
28
  end
@@ -14,14 +14,24 @@ module WebAuthn
14
14
  end
15
15
 
16
16
  class CredentialCreationOptions < CredentialOptions
17
- DEFAULT_ALGORITHMS = ["ES256", "RS256"].freeze
18
17
  DEFAULT_RP_NAME = "web-server"
19
18
 
20
- DEFAULT_PUB_KEY_CRED_PARAMS = DEFAULT_ALGORITHMS.map do |alg_name|
21
- { type: "public-key", alg: COSE::Algorithm.by_name(alg_name).id }
22
- end.freeze
19
+ attr_accessor :attestation, :authenticator_selection, :exclude_credentials, :extensions
23
20
 
24
- def initialize(user_id:, user_name:, user_display_name: nil, rp_name: nil)
21
+ def initialize(
22
+ attestation: nil,
23
+ authenticator_selection: nil,
24
+ exclude_credentials: nil,
25
+ extensions: nil,
26
+ user_id:,
27
+ user_name:,
28
+ user_display_name: nil,
29
+ rp_name: nil
30
+ )
31
+ @attestation = attestation
32
+ @authenticator_selection = authenticator_selection
33
+ @exclude_credentials = exclude_credentials
34
+ @extensions = extensions
25
35
  @user_id = user_id
26
36
  @user_name = user_name
27
37
  @user_display_name = user_display_name
@@ -29,16 +39,36 @@ module WebAuthn
29
39
  end
30
40
 
31
41
  def to_h
32
- {
42
+ options = {
33
43
  challenge: challenge,
34
44
  pubKeyCredParams: pub_key_cred_params,
35
45
  user: { id: user.id, name: user.name, displayName: user.display_name },
36
46
  rp: { name: rp.name }
37
47
  }
48
+
49
+ if attestation
50
+ options[:attestation] = attestation
51
+ end
52
+
53
+ if authenticator_selection
54
+ options[:authenticatorSelection] = authenticator_selection
55
+ end
56
+
57
+ if exclude_credentials
58
+ options[:excludeCredentials] = exclude_credentials
59
+ end
60
+
61
+ if extensions
62
+ options[:extensions] = extensions
63
+ end
64
+
65
+ options
38
66
  end
39
67
 
40
68
  def pub_key_cred_params
41
- DEFAULT_PUB_KEY_CRED_PARAMS
69
+ WebAuthn.configuration.algorithms.map do |alg_name|
70
+ { type: "public-key", alg: COSE::Algorithm.by_name(alg_name).id }
71
+ end
42
72
  end
43
73
 
44
74
  def rp
@@ -8,12 +8,26 @@ module WebAuthn
8
8
  end
9
9
 
10
10
  class CredentialRequestOptions < CredentialOptions
11
- def to_h
12
- { challenge: challenge, allowCredentials: allow_credentials }
11
+ attr_accessor :allow_credentials, :extensions, :user_verification
12
+
13
+ def initialize(allow_credentials: [], extensions: nil, user_verification: nil)
14
+ @allow_credentials = allow_credentials
15
+ @extensions = extensions
16
+ @user_verification = user_verification
13
17
  end
14
18
 
15
- def allow_credentials
16
- []
19
+ def to_h
20
+ options = { challenge: challenge, allowCredentials: allow_credentials }
21
+
22
+ if extensions
23
+ options[:extensions] = extensions
24
+ end
25
+
26
+ if user_verification
27
+ options[:userVerification] = user_verification
28
+ end
29
+
30
+ options
17
31
  end
18
32
  end
19
33
  end
@@ -9,23 +9,34 @@ module WebAuthn
9
9
  class AuthenticatorData
10
10
  AAGUID = SecureRandom.random_bytes(16)
11
11
 
12
- def initialize(rp_id_hash:, credential: nil, sign_count: 0, user_present: true, user_verified: !user_present,
13
- aaguid: AAGUID)
12
+ def initialize(
13
+ rp_id_hash:,
14
+ credential: {
15
+ id: SecureRandom.random_bytes(16),
16
+ public_key: OpenSSL::PKey::EC.new("prime256v1").generate_key.public_key
17
+ },
18
+ sign_count: 0,
19
+ user_present: true,
20
+ user_verified: !user_present,
21
+ aaguid: AAGUID,
22
+ extensions: { "fakeExtension" => "fakeExtensionValue" }
23
+ )
14
24
  @rp_id_hash = rp_id_hash
15
25
  @credential = credential
16
26
  @sign_count = sign_count
17
27
  @user_present = user_present
18
28
  @user_verified = user_verified
19
29
  @aaguid = aaguid
30
+ @extensions = extensions
20
31
  end
21
32
 
22
33
  def serialize
23
- rp_id_hash + flags + serialized_sign_count + attested_credential_data + extensions
34
+ rp_id_hash + flags + serialized_sign_count + attested_credential_data + extension_data
24
35
  end
25
36
 
26
37
  private
27
38
 
28
- attr_reader :rp_id_hash, :credential, :sign_count, :user_present, :user_verified
39
+ attr_reader :rp_id_hash, :credential, :sign_count, :user_present, :user_verified, :extensions
29
40
 
30
41
  def flags
31
42
  [
@@ -58,8 +69,12 @@ module WebAuthn
58
69
  end
59
70
  end
60
71
 
61
- def extensions
62
- CBOR.encode("fakeExtension" => "fakeExtensionValue")
72
+ def extension_data
73
+ if extensions
74
+ CBOR.encode(extensions)
75
+ else
76
+ ""
77
+ end
63
78
  end
64
79
 
65
80
  def bit(flag)
@@ -79,7 +94,7 @@ module WebAuthn
79
94
  end
80
95
 
81
96
  def extension_data_included_bit
82
- if extensions.empty?
97
+ if extension_data.empty?
83
98
  "0"
84
99
  else
85
100
  "1"
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module WebAuthn
6
+ class PublicKeyCredential
7
+ VALID_TYPE = "public-key"
8
+
9
+ attr_reader :type, :id, :raw_id, :response
10
+
11
+ def initialize(type:, id:, raw_id:, response:)
12
+ @type = type
13
+ @id = id
14
+ @raw_id = raw_id
15
+ @response = response
16
+ end
17
+
18
+ def verify(*args)
19
+ valid_type? || raise("invalid type")
20
+ valid_id? || raise("invalid id")
21
+ response.verify(*args)
22
+
23
+ true
24
+ end
25
+
26
+ private
27
+
28
+ def valid_type?
29
+ type == VALID_TYPE
30
+ end
31
+
32
+ def valid_id?
33
+ raw_id && id && raw_id == Base64.urlsafe_decode64(id)
34
+ end
35
+ end
36
+ end
@@ -23,7 +23,12 @@ module WebAuthn
23
23
  end
24
24
 
25
25
  def verify(signature, verification_data)
26
- public_key.verify(cose_algorithm.hash, signature, verification_data)
26
+ if rsa_pss?
27
+ public_key.verify_pss(cose_algorithm.hash, signature, verification_data,
28
+ salt_length: :digest, mgf1_hash: cose_algorithm.hash)
29
+ else
30
+ public_key.verify(cose_algorithm.hash, signature, verification_data)
31
+ end
27
32
  end
28
33
 
29
34
  private
@@ -39,14 +44,22 @@ module WebAuthn
39
44
  end
40
45
  end
41
46
 
47
+ def rsa_pss?
48
+ cose_algorithm.name.start_with?("PS")
49
+ end
50
+
42
51
  def validate
43
- if cose_algorithm
44
- if !KTY_MAP[cose_algorithm.kty].include?(public_key.class)
45
- raise("Incompatible algorithm and key")
46
- end
47
- else
52
+ if !cose_algorithm
53
+ raise UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}"
54
+ elsif !supported_algorithms.include?(cose_algorithm.name)
48
55
  raise UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}"
56
+ elsif !KTY_MAP[cose_algorithm.kty].include?(public_key.class)
57
+ raise("Incompatible algorithm and key")
49
58
  end
50
59
  end
60
+
61
+ def supported_algorithms
62
+ WebAuthn.configuration.algorithms
63
+ end
51
64
  end
52
65
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebAuthn
4
- VERSION = "1.15.0"
4
+ VERSION = "1.16.0"
5
5
  end
data/lib/webauthn.rb CHANGED
@@ -5,4 +5,5 @@ require "webauthn/authenticator_assertion_response"
5
5
  require "webauthn/configuration"
6
6
  require "webauthn/credential_creation_options"
7
7
  require "webauthn/credential_request_options"
8
+ require "webauthn/public_key_credential"
8
9
  require "webauthn/version"
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.15.0
4
+ version: 1.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gonzalo Rodriguez
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2019-05-16 00:00:00.000000000 Z
12
+ date: 2019-06-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bindata
@@ -256,6 +256,7 @@ files:
256
256
  - lib/webauthn/fake_authenticator/attestation_object.rb
257
257
  - lib/webauthn/fake_authenticator/authenticator_data.rb
258
258
  - lib/webauthn/fake_client.rb
259
+ - lib/webauthn/public_key_credential.rb
259
260
  - lib/webauthn/security_utils.rb
260
261
  - lib/webauthn/signature_verifier.rb
261
262
  - lib/webauthn/version.rb