webauthn-slim 3.4.3

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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/actions/install-openssl/action.yml +35 -0
  4. data/.github/actions/install-ruby/action.yml +76 -0
  5. data/.github/dependabot.yml +6 -0
  6. data/.github/workflows/build.yml +49 -0
  7. data/.github/workflows/git.yml +21 -0
  8. data/.github/workflows/release.yml +28 -0
  9. data/.gitignore +16 -0
  10. data/.rspec +2 -0
  11. data/.rubocop.yml +306 -0
  12. data/CHANGELOG.md +462 -0
  13. data/CONTRIBUTING.md +39 -0
  14. data/Gemfile +8 -0
  15. data/LICENSE.txt +21 -0
  16. data/README.md +479 -0
  17. data/Rakefile +10 -0
  18. data/SECURITY.md +21 -0
  19. data/bin/console +15 -0
  20. data/bin/setup +8 -0
  21. data/docs/advanced_configuration.md +173 -0
  22. data/docs/u2f_migration.md +90 -0
  23. data/lib/webauthn/attestation_object.rb +47 -0
  24. data/lib/webauthn/attestation_statement/apple.rb +65 -0
  25. data/lib/webauthn/attestation_statement/base.rb +175 -0
  26. data/lib/webauthn/attestation_statement/fido_u2f/public_key.rb +44 -0
  27. data/lib/webauthn/attestation_statement/fido_u2f.rb +71 -0
  28. data/lib/webauthn/attestation_statement/none.rb +23 -0
  29. data/lib/webauthn/attestation_statement/packed.rb +72 -0
  30. data/lib/webauthn/attestation_statement.rb +35 -0
  31. data/lib/webauthn/authenticator_assertion_response.rb +87 -0
  32. data/lib/webauthn/authenticator_attestation_response.rb +84 -0
  33. data/lib/webauthn/authenticator_data/attested_credential_data.rb +94 -0
  34. data/lib/webauthn/authenticator_data.rb +130 -0
  35. data/lib/webauthn/authenticator_response.rb +137 -0
  36. data/lib/webauthn/cbor.rb +189 -0
  37. data/lib/webauthn/client_data.rb +67 -0
  38. data/lib/webauthn/configuration.rb +68 -0
  39. data/lib/webauthn/cose.rb +237 -0
  40. data/lib/webauthn/credential.rb +26 -0
  41. data/lib/webauthn/credential_creation_options.rb +97 -0
  42. data/lib/webauthn/credential_entity.rb +11 -0
  43. data/lib/webauthn/credential_options.rb +17 -0
  44. data/lib/webauthn/credential_request_options.rb +44 -0
  45. data/lib/webauthn/credential_rp_entity.rb +8 -0
  46. data/lib/webauthn/credential_user_entity.rb +16 -0
  47. data/lib/webauthn/encoder.rb +18 -0
  48. data/lib/webauthn/encoders.rb +62 -0
  49. data/lib/webauthn/error.rb +5 -0
  50. data/lib/webauthn/fake_authenticator/attestation_object.rb +90 -0
  51. data/lib/webauthn/fake_authenticator/authenticator_data.rb +156 -0
  52. data/lib/webauthn/fake_authenticator.rb +134 -0
  53. data/lib/webauthn/fake_client.rb +179 -0
  54. data/lib/webauthn/json_serializer.rb +45 -0
  55. data/lib/webauthn/public_key.rb +65 -0
  56. data/lib/webauthn/public_key_credential/creation_options.rb +93 -0
  57. data/lib/webauthn/public_key_credential/entity.rb +21 -0
  58. data/lib/webauthn/public_key_credential/options.rb +51 -0
  59. data/lib/webauthn/public_key_credential/request_options.rb +46 -0
  60. data/lib/webauthn/public_key_credential/rp_entity.rb +23 -0
  61. data/lib/webauthn/public_key_credential/user_entity.rb +24 -0
  62. data/lib/webauthn/public_key_credential.rb +94 -0
  63. data/lib/webauthn/public_key_credential_with_assertion.rb +49 -0
  64. data/lib/webauthn/public_key_credential_with_attestation.rb +30 -0
  65. data/lib/webauthn/relying_party.rb +150 -0
  66. data/lib/webauthn/u2f_migrator.rb +76 -0
  67. data/lib/webauthn/version.rb +5 -0
  68. data/lib/webauthn.rb +19 -0
  69. data/webauthn.gemspec +43 -0
  70. metadata +218 -0
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module WebAuthn
6
+ class CredentialOptions
7
+ CHALLENGE_LENGTH = 32
8
+
9
+ def challenge
10
+ @challenge ||= SecureRandom.random_bytes(CHALLENGE_LENGTH)
11
+ end
12
+
13
+ def timeout
14
+ @timeout = WebAuthn.configuration.credential_options_timeout
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/credential_options"
4
+
5
+ module WebAuthn
6
+ def self.credential_request_options
7
+ warn(
8
+ "DEPRECATION WARNING: `WebAuthn.credential_request_options` is deprecated."\
9
+ " Please use `WebAuthn::Credential.options_for_get` instead."
10
+ )
11
+
12
+ CredentialRequestOptions.new.to_h
13
+ end
14
+
15
+ class CredentialRequestOptions < CredentialOptions
16
+ attr_accessor :allow_credentials, :extensions, :user_verification
17
+
18
+ def initialize(allow_credentials: [], extensions: nil, user_verification: nil)
19
+ super()
20
+
21
+ @allow_credentials = allow_credentials
22
+ @extensions = extensions
23
+ @user_verification = user_verification
24
+ end
25
+
26
+ def to_h
27
+ options = {
28
+ challenge: challenge,
29
+ timeout: timeout,
30
+ allowCredentials: allow_credentials
31
+ }
32
+
33
+ if extensions
34
+ options[:extensions] = extensions
35
+ end
36
+
37
+ if user_verification
38
+ options[:userVerification] = user_verification
39
+ end
40
+
41
+ options
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/credential_entity"
4
+
5
+ module WebAuthn
6
+ class CredentialRPEntity < CredentialEntity
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/credential_entity"
4
+
5
+ module WebAuthn
6
+ class CredentialUserEntity < CredentialEntity
7
+ attr_reader :id, :display_name
8
+
9
+ def initialize(id:, display_name: nil, **keyword_arguments)
10
+ super(**keyword_arguments)
11
+
12
+ @id = id
13
+ @display_name = display_name || name
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/encoders"
4
+
5
+ module WebAuthn
6
+ class Encoder
7
+ extend Forwardable
8
+
9
+ # https://www.w3.org/TR/webauthn-2/#base64url-encoding
10
+ STANDARD_ENCODING = :base64url
11
+
12
+ def_delegators :@encoder_klass, :encode, :decode
13
+
14
+ def initialize(encoding = STANDARD_ENCODING)
15
+ @encoder_klass = Encoders.lookup(encoding)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebAuthn
4
+ def self.standard_encoder
5
+ @standard_encoder ||= Encoders.lookup(Encoder::STANDARD_ENCODING)
6
+ end
7
+
8
+ module Encoders
9
+ class << self
10
+ def lookup(encoding)
11
+ case encoding
12
+ when :base64
13
+ Base64Encoder
14
+ when :base64url
15
+ Base64UrlEncoder
16
+ when nil, false
17
+ NullEncoder
18
+ else
19
+ raise "Unsupported or unknown encoding: #{encoding}"
20
+ end
21
+ end
22
+ end
23
+
24
+ class Base64Encoder
25
+ def self.encode(data)
26
+ [data].pack("m0") # Base64.strict_encode64(data)
27
+ end
28
+
29
+ def self.decode(data)
30
+ data.unpack1("m0") # Base64.strict_decode64(data)
31
+ end
32
+ end
33
+
34
+ class Base64UrlEncoder
35
+ def self.encode(data)
36
+ data = [data].pack("m0") # Base64.urlsafe_encode64(data, padding: false)
37
+ data.chomp!("==") or data.chomp!("=")
38
+ data.tr!("+/", "-_")
39
+ data
40
+ end
41
+
42
+ def self.decode(data)
43
+ if !data.end_with?("=") && data.length % 4 != 0 # Base64.urlsafe_decode64(data)
44
+ data = data.ljust((data.length + 3) & ~3, "=")
45
+ end
46
+
47
+ data = data.tr("-_", "+/")
48
+ data.unpack1("m0")
49
+ end
50
+ end
51
+
52
+ class NullEncoder
53
+ def self.encode(data)
54
+ data
55
+ end
56
+
57
+ def self.decode(data)
58
+ data
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebAuthn
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cbor"
4
+ require "webauthn/fake_authenticator/authenticator_data"
5
+
6
+ module WebAuthn
7
+ class FakeAuthenticator
8
+ class AttestationObject
9
+ def initialize(
10
+ client_data_hash:,
11
+ rp_id_hash:,
12
+ credential_id:,
13
+ credential_key:,
14
+ user_present: true,
15
+ user_verified: false,
16
+ backup_eligibility: false,
17
+ backup_state: false,
18
+ attested_credential_data: true,
19
+ sign_count: 0,
20
+ extensions: nil
21
+ )
22
+ @client_data_hash = client_data_hash
23
+ @rp_id_hash = rp_id_hash
24
+ @credential_id = credential_id
25
+ @credential_key = credential_key
26
+ @user_present = user_present
27
+ @user_verified = user_verified
28
+ @backup_eligibility = backup_eligibility
29
+ @backup_state = backup_state
30
+ @attested_credential_data = attested_credential_data
31
+ @sign_count = sign_count
32
+ @extensions = extensions
33
+ end
34
+
35
+ def serialize
36
+ CBOR.encode(
37
+ "fmt" => "none",
38
+ "attStmt" => {},
39
+ "authData" => authenticator_data.serialize
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader(
46
+ :client_data_hash,
47
+ :rp_id_hash,
48
+ :credential_id,
49
+ :credential_key,
50
+ :user_present,
51
+ :user_verified,
52
+ :backup_eligibility,
53
+ :backup_state,
54
+ :attested_credential_data,
55
+ :sign_count,
56
+ :extensions
57
+ )
58
+
59
+ def authenticator_data
60
+ @authenticator_data ||=
61
+ begin
62
+ credential_data =
63
+ if attested_credential_data
64
+ { id: credential_id, public_key: credential_public_key }
65
+ end
66
+
67
+ AuthenticatorData.new(
68
+ rp_id_hash: rp_id_hash,
69
+ credential: credential_data,
70
+ user_present: user_present,
71
+ user_verified: user_verified,
72
+ backup_eligibility: backup_eligibility,
73
+ backup_state: backup_state,
74
+ sign_count: 0,
75
+ extensions: extensions
76
+ )
77
+ end
78
+ end
79
+
80
+ def credential_public_key
81
+ case credential_key
82
+ when OpenSSL::PKey::RSA, OpenSSL::PKey::EC
83
+ credential_key.public_key
84
+ when OpenSSL::PKey::PKey
85
+ OpenSSL::PKey.read(credential_key.public_to_der)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cose"
4
+ require "webauthn/cbor"
5
+ require "securerandom"
6
+
7
+ module WebAuthn
8
+ class FakeAuthenticator
9
+ class AuthenticatorData
10
+ AAGUID = SecureRandom.random_bytes(16)
11
+
12
+ attr_reader :sign_count
13
+
14
+ def initialize(
15
+ rp_id_hash:,
16
+ credential: {
17
+ id: SecureRandom.random_bytes(16),
18
+ public_key: OpenSSL::PKey::EC.generate("prime256v1").public_key
19
+ },
20
+ sign_count: 0,
21
+ user_present: true,
22
+ user_verified: !user_present,
23
+ backup_eligibility: false,
24
+ backup_state: false,
25
+ aaguid: AAGUID,
26
+ extensions: { "fakeExtension" => "fakeExtensionValue" }
27
+ )
28
+ @rp_id_hash = rp_id_hash
29
+ @credential = credential
30
+ @sign_count = sign_count
31
+ @user_present = user_present
32
+ @user_verified = user_verified
33
+ @backup_eligibility = backup_eligibility
34
+ @backup_state = backup_state
35
+ @aaguid = aaguid
36
+ @extensions = extensions
37
+ end
38
+
39
+ def serialize
40
+ rp_id_hash + flags + serialized_sign_count + attested_credential_data + extension_data
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :rp_id_hash,
46
+ :credential,
47
+ :user_present,
48
+ :user_verified,
49
+ :extensions,
50
+ :backup_eligibility,
51
+ :backup_state
52
+
53
+ def flags
54
+ [
55
+ [
56
+ bit(:user_present),
57
+ reserved_for_future_use_bit,
58
+ bit(:user_verified),
59
+ bit(:backup_eligibility),
60
+ bit(:backup_state),
61
+ reserved_for_future_use_bit,
62
+ attested_credential_data_included_bit,
63
+ extension_data_included_bit
64
+ ].join
65
+ ].pack("b*")
66
+ end
67
+
68
+ def serialized_sign_count
69
+ [sign_count].pack('L>')
70
+ end
71
+
72
+ def attested_credential_data
73
+ @attested_credential_data ||=
74
+ if credential
75
+ @aaguid +
76
+ [credential[:id].length].pack("n*") +
77
+ credential[:id] +
78
+ cose_credential_public_key
79
+ else
80
+ ""
81
+ end
82
+ end
83
+
84
+ def extension_data
85
+ if extensions
86
+ CBOR.encode(extensions)
87
+ else
88
+ ""
89
+ end
90
+ end
91
+
92
+ def bit(flag)
93
+ if context[flag]
94
+ "1"
95
+ else
96
+ "0"
97
+ end
98
+ end
99
+
100
+ def attested_credential_data_included_bit
101
+ if attested_credential_data.empty?
102
+ "0"
103
+ else
104
+ "1"
105
+ end
106
+ end
107
+
108
+ def extension_data_included_bit
109
+ if extension_data.empty?
110
+ "0"
111
+ else
112
+ "1"
113
+ end
114
+ end
115
+
116
+ def reserved_for_future_use_bit
117
+ "0"
118
+ end
119
+
120
+ def context
121
+ {
122
+ user_present: user_present,
123
+ user_verified: user_verified,
124
+ backup_eligibility: backup_eligibility,
125
+ backup_state: backup_state
126
+ }
127
+ end
128
+
129
+ def cose_credential_public_key
130
+ case credential[:public_key]
131
+ when OpenSSL::PKey::RSA
132
+ key = COSE::Key::RSA.from_pkey(credential[:public_key])
133
+ key.alg = -257
134
+ when OpenSSL::PKey::EC::Point
135
+ alg = {
136
+ COSE::Key::Curve.by_name("P-256").id => -7,
137
+ COSE::Key::Curve.by_name("P-384").id => -35,
138
+ COSE::Key::Curve.by_name("P-521").id => -36
139
+ }
140
+
141
+ key = COSE::Key::EC2.from_pkey(credential[:public_key])
142
+ key.alg = alg[key.crv]
143
+ when OpenSSL::PKey::PKey
144
+ key = COSE::Key::OKP.from_pkey(credential[:public_key])
145
+ key.alg = -8
146
+ end
147
+
148
+ key.serialize
149
+ end
150
+
151
+ def key_bytes(public_key)
152
+ public_key.to_bn.to_s(2)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cbor"
4
+ require "openssl"
5
+ require "securerandom"
6
+ require "webauthn/fake_authenticator/attestation_object"
7
+ require "webauthn/fake_authenticator/authenticator_data"
8
+
9
+ module WebAuthn
10
+ class FakeAuthenticator
11
+ def initialize
12
+ @credentials = {}
13
+ end
14
+
15
+ def make_credential(
16
+ rp_id:,
17
+ client_data_hash:,
18
+ user_present: true,
19
+ user_verified: false,
20
+ backup_eligibility: false,
21
+ backup_state: false,
22
+ attested_credential_data: true,
23
+ algorithm: nil,
24
+ sign_count: nil,
25
+ extensions: nil
26
+ )
27
+ credential_id, credential_key, credential_sign_count = new_credential(algorithm)
28
+ sign_count ||= credential_sign_count
29
+
30
+ credentials[rp_id] ||= {}
31
+ credentials[rp_id][credential_id] = {
32
+ credential_key: credential_key,
33
+ sign_count: sign_count + 1
34
+ }
35
+
36
+ AttestationObject.new(
37
+ client_data_hash: client_data_hash,
38
+ rp_id_hash: hashed(rp_id),
39
+ credential_id: credential_id,
40
+ credential_key: credential_key,
41
+ user_present: user_present,
42
+ user_verified: user_verified,
43
+ backup_eligibility: backup_eligibility,
44
+ backup_state: backup_state,
45
+ attested_credential_data: attested_credential_data,
46
+ sign_count: sign_count,
47
+ extensions: extensions
48
+ ).serialize
49
+ end
50
+
51
+ def get_assertion(
52
+ rp_id:,
53
+ client_data_hash:,
54
+ user_present: true,
55
+ user_verified: false,
56
+ backup_eligibility: false,
57
+ backup_state: false,
58
+ aaguid: AuthenticatorData::AAGUID,
59
+ sign_count: nil,
60
+ extensions: nil,
61
+ allow_credentials: nil
62
+ )
63
+ credential_options = credentials[rp_id]
64
+
65
+ if credential_options
66
+ allow_credentials ||= credential_options.keys
67
+ credential_id = (credential_options.keys & allow_credentials).first
68
+ unless credential_id
69
+ raise "No matching credentials (allowed=#{allow_credentials}) " \
70
+ "found for RP #{rp_id} among credentials=#{credential_options}"
71
+ end
72
+
73
+ credential = credential_options[credential_id]
74
+ credential_key = credential[:credential_key]
75
+ credential_sign_count = credential[:sign_count]
76
+
77
+ authenticator_data = AuthenticatorData.new(
78
+ rp_id_hash: hashed(rp_id),
79
+ user_present: user_present,
80
+ user_verified: user_verified,
81
+ backup_eligibility: backup_eligibility,
82
+ backup_state: backup_state,
83
+ aaguid: aaguid,
84
+ credential: nil,
85
+ sign_count: sign_count || credential_sign_count,
86
+ extensions: extensions
87
+ ).serialize
88
+
89
+ signature_digest_algorithm =
90
+ case credential_key
91
+ when OpenSSL::PKey::RSA, OpenSSL::PKey::EC
92
+ 'SHA256'
93
+ when OpenSSL::PKey::PKey
94
+ nil
95
+ end
96
+ signature = credential_key.sign(signature_digest_algorithm, authenticator_data + client_data_hash)
97
+ credential[:sign_count] += 1
98
+
99
+ {
100
+ credential_id: credential_id,
101
+ authenticator_data: authenticator_data,
102
+ signature: signature
103
+ }
104
+ else
105
+ raise "No credentials found for RP #{rp_id}"
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ attr_reader :credentials
112
+
113
+ def new_credential(algorithm)
114
+ algorithm ||= 'ES256'
115
+ credential_key =
116
+ case algorithm
117
+ when 'ES256'
118
+ OpenSSL::PKey::EC.generate('prime256v1')
119
+ when 'RS256'
120
+ OpenSSL::PKey::RSA.new(2048)
121
+ when 'EdDSA'
122
+ OpenSSL::PKey.generate_key("ED25519")
123
+ else
124
+ raise "Unsupported algorithm #{algorithm}"
125
+ end
126
+
127
+ [SecureRandom.random_bytes(16), credential_key, 0]
128
+ end
129
+
130
+ def hashed(target)
131
+ OpenSSL::Digest::SHA256.digest(target)
132
+ end
133
+ end
134
+ end