webauthn 1.15.0 → 1.16.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 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