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,173 @@
1
+ # Advanced Configuration
2
+
3
+ ## Global vs Instance Based Configuration
4
+
5
+ Which approach suits best your needs will depend on the architecture of your application and how do your users need to register and authenticate to it.
6
+
7
+ If you have a multi-tenant application, or any application segmentation, where your users register and authenticate to each of these tenants or segments individually using different hostnames, or with different security needs, you need to go through [Instance Based Configuration](#instance-based-configuration).
8
+
9
+ However, if your application is served for just one hostname, or else if your users authenticate to only one subdomain (e.g. your application serves www.example.com and admin.example.com but all your users authenticate through auth.example.com) you can still rely on one [Global Configuration](../README.md#configuration).
10
+
11
+ If you are still not sure, or want to keep your options open, be aware that [Instance Based Configuration](#instance-based-configuration) is also a valid way of defining a single instance configuration and how you share such configuration across your application, it's up to you.
12
+
13
+
14
+ ## Instance Based Configuration
15
+
16
+ Instead of the [Global Configuration](../README.md#configuration) you place in `config/initializers/webauthn.rb`,
17
+ you can now have an on-demand instance of `WebAuthn::RelyingParty` with the same configuration options, that
18
+ you can build anywhere in your application, in the following way:
19
+
20
+ ```ruby
21
+ relying_party = WebAuthn::RelyingParty.new(
22
+ # This value needs to match `window.location.origin` evaluated by
23
+ # the User Agent during registration and authentication ceremonies.
24
+ # Multiple origins can be used when needed. Using more than one will imply you MUST configure rp_id explicitely. If you need your credentials to be bound to a single origin but you have more than one tenant, please see [our Advanced Configuration section](https://github.com/cedarcode/webauthn-ruby/blob/master/docs/advanced_configuration.md) instead of adding multiple origins.
25
+ allowed_origins: ["https://admin.example.com"],
26
+
27
+ # Relying Party name for display purposes
28
+ name: "Admin Site for Example Inc."
29
+
30
+ # Optionally configure a client timeout hint, in milliseconds.
31
+ # This hint specifies how long the browser should wait for any
32
+ # interaction with the user.
33
+ # This hint may be overridden by the browser.
34
+ # https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-timeout
35
+ # credential_options_timeout: 120_000
36
+
37
+ # You can optionally specify a different Relying Party ID
38
+ # (https://www.w3.org/TR/webauthn/#relying-party-identifier)
39
+ # if it differs from the default one.
40
+ #
41
+ # In this case the default would be "admin.example.com", but you can set it to
42
+ # the suffix "example.com"
43
+ #
44
+ # id: "example.com"
45
+
46
+ # Configure preferred binary-to-text encoding scheme. This should match the encoding scheme
47
+ # used in your client-side (user agent) code before sending the credential to the server.
48
+ # Supported values: `:base64url` (default), `:base64` or `false` to disable all encoding.
49
+ #
50
+ # encoding: :base64url
51
+
52
+ # Possible values: "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512", "RS1"
53
+ # Default: ["ES256", "PS256", "RS256"]
54
+ #
55
+ # algorithms: ["ES384"]
56
+ )
57
+ ```
58
+
59
+ ## Instance Based API
60
+
61
+ The explanation for each ceremony can be found in depth in [Credential Registration](../README.md#credential-registration) and [Credential Authentication](../README.md#credential-authentication) but if you choose this instance based approach to define your WebAuthn configurations and assuming `relying_party` is the result of an instance you get through `WebAuthn::RelyingParty.new(...)` the code in those explanations needs to be updated to:
62
+
63
+ ### Credential Registration
64
+
65
+ #### Initiation phase
66
+
67
+ ```ruby
68
+ # Generate and store the WebAuthn User ID the first time the user registers a credential
69
+ if !user.webauthn_id
70
+ user.update!(webauthn_id: WebAuthn.generate_user_id)
71
+ end
72
+
73
+ options = relying_party.options_for_registration(
74
+ user: { id: user.webauthn_id, name: user.name },
75
+ exclude: user.credentials.map { |c| c.external_id }
76
+ )
77
+
78
+ # Store the newly generated challenge somewhere so you can have it
79
+ # for the verification phase.
80
+ session[:creation_challenge] = options.challenge
81
+
82
+ # Send `options` back to the browser, so that they can be used
83
+ # to call `navigator.credentials.create({ "publicKey": options })`
84
+ #
85
+ # You can call `options.as_json` to get a ruby hash with a JSON representation if needed.
86
+
87
+ # If inside a Rails controller, `render json: options` will just work.
88
+ # I.e. it will encode and convert the options to JSON automatically.
89
+
90
+ # For your frontend code, you might find @github/webauthn-json npm package useful.
91
+ # Especially for handling the necessary decoding of the options, and sending the
92
+ # `PublicKeyCredential` object back to the server.
93
+ ```
94
+
95
+ #### Verification phase
96
+
97
+ ```ruby
98
+ # Assuming you're using @github/webauthn-json package to send the `PublicKeyCredential` object back
99
+ # in params[:publicKeyCredential]:
100
+ begin
101
+ webauthn_credential = relying_party.verify_registration(
102
+ params[:publicKeyCredential],
103
+ session[:creation_challenge]
104
+ )
105
+
106
+ # Store Credential ID, Credential Public Key and Sign Count for future authentications
107
+ user.credentials.create!(
108
+ external_id: webauthn_credential.id,
109
+ public_key: webauthn_credential.public_key,
110
+ sign_count: webauthn_credential.sign_count
111
+ )
112
+ rescue WebAuthn::Error => e
113
+ # Handle error
114
+ end
115
+ ```
116
+
117
+ ### Credential Authentication
118
+
119
+ #### Initiation phase
120
+
121
+ ```ruby
122
+ options = relying_party.options_for_authentication(allow: user.credentials.map { |c| c.webauthn_id })
123
+
124
+ # Store the newly generated challenge somewhere so you can have it
125
+ # for the verification phase.
126
+ session[:authentication_challenge] = options.challenge
127
+
128
+ # Send `options` back to the browser, so that they can be used
129
+ # to call `navigator.credentials.get({ "publicKey": options })`
130
+
131
+ # You can call `options.as_json` to get a ruby hash with a JSON representation if needed.
132
+
133
+ # If inside a Rails controller, `render json: options` will just work.
134
+ # I.e. it will encode and convert the options to JSON automatically.
135
+
136
+ # For your frontend code, you might find @github/webauthn-json npm package useful.
137
+ # Especially for handling the necessary decoding of the options, and sending the
138
+ # `PublicKeyCredential` object back to the server.
139
+ ```
140
+
141
+ #### Verification phase
142
+
143
+ ```ruby
144
+ begin
145
+ # Assuming you're using @github/webauthn-json package to send the `PublicKeyCredential` object back
146
+ # in params[:publicKeyCredential]:
147
+ webauthn_credential, stored_credential = relying_party.verify_authentication(
148
+ params[:publicKeyCredential],
149
+ session[:authentication_challenge]
150
+ ) do |webauthn_credential|
151
+ # the returned object needs to respond to #public_key and #sign_count
152
+ user.credentials.find_by(external_id: webauthn_credential.id)
153
+ end
154
+
155
+ # Update the stored credential sign count with the value from `webauthn_credential.sign_count`
156
+ stored_credential.update!(sign_count: webauthn_credential.sign_count)
157
+
158
+ # Continue with successful sign in or 2FA verification...
159
+
160
+ rescue WebAuthn::SignCountVerificationError => e
161
+ # Cryptographic verification of the authenticator data succeeded, but the signature counter was less than or equal
162
+ # to the stored value. This can have several reasons and depending on your risk tolerance you can choose to fail or
163
+ # pass authentication. For more information see https://www.w3.org/TR/webauthn/#sign-counter
164
+ rescue WebAuthn::Error => e
165
+ # Handle error
166
+ end
167
+ ```
168
+
169
+ ## Moving from Global to Instance Based Configuration
170
+
171
+ Adding a configuration for a new instance does not mean you need to get rid of your Global configuration. They can co-exist in your application and be both available for the different usages you might have. `WebAuthn.configuration.relying_party` will always return the global one while `WebAuthn::RelyingParty.new`, executed anywhere in your codebase, will allow you to create a different instance as you see the need. They will not collide and instead operate in isolation without any shared state.
172
+
173
+ The gem API described in the current [Usage](../README.md#usage) section for the [Global Configuration](../README.md#configuration) approach will still be valid but the [Instance Based API](#instance-based-api) also works with the global `relying_party` that is maintained globally at `WebAuthn.configuration.relying_party`.
@@ -0,0 +1,90 @@
1
+ # Migrating from U2F to WebAuthn
2
+
3
+ The Chromium team [recommends](https://groups.google.com/a/chromium.org/forum/#!msg/security-dev/BGWA1d7a6rI/W2avestmBAAJ)
4
+ application developers to switch from the U2F API to the WebAuthn API. This document describes how a Ruby application
5
+ using the [u2f gem by Castle](https://github.com/castle/ruby-u2f) can migrate existing credentials so that their users
6
+ do not experience interruption or need to re-register their security keys.
7
+
8
+ Note that the migration is one-way: credentials registered using WebAuthn cannot be made compatible with the U2F API.
9
+ It is recommended to successfully migrate authorization flows before migrating registration flows.
10
+
11
+ ## Migrate registered U2F credentials
12
+
13
+ Assuming you have a registered credential per the u2f gem readme, base64 urlsafe encoded in a database:
14
+
15
+ ```ruby
16
+ # This domain will be used in all code examples. It's a single-facet app but a multi-facet AppID
17
+ # (e.g. https://example.com/app-id.json) will work as well.
18
+ domain = URI("https://login.example.com")
19
+
20
+ u2f_registration = U2F::U2F.new(domain.to_s).register!(u2f_challenge, u2f_register_response)
21
+ # => #<U2F::Registration:0x00007fd62f43d688
22
+ # @certificate=
23
+ # "MIIBCzCBsgIBATAKBggqhkjOPQQDAjASMRAwDgYDVQQDDAdVMkZUZXN0MB4XDTE5MDUzMDE3MjIwM1oXDTIwMDUyOTE3MjIwM1owEjEQMA4GA1UEAwwHVTJGVGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBMKpejumzlH6NxWIx2Ol+EManS9oX5nguG4RT43rZNkyn/zGjdJEhXksN5zT34rLZgFheBgkDGJCdtTPlhVK10wCgYIKoZIzj0EAwIDSAAwRQIhALYxFcROCeifWpv5+wNZIiaO/bGQg8rFBHCw3aHgehdZAiBJ3xFmQh7+Gjxt6CeAcY/k/VVAYu2vP4sUqXnCQFgJUA==",
24
+ # @key_handle="mbVMRTgzST5xLumckGztJ9VFW6veObfNIYWSn3sTqIY",
25
+ # @public_key="BOJQXlFg+ZfZKm48FNq2Ye5vSwOscE1i7YsGRSIjIe3GI0OXrSBDADDn0dQlz2iDzZ7LvCwiHz72U1qhVas3vus=">
26
+ ```
27
+
28
+ The `U2fMigrator` class quacks like `WebAuthn::AuthenticatorAttestationResponse` and can be used similarly as documented
29
+ in the [registration verification phase](https://github.com/cedarcode/webauthn-ruby/blob/master/README.md#verification-phase).
30
+ Of course a `verify` instance method is not implemented, as there is no real interaction with an authenticator.
31
+
32
+ The migrator can be used to convert credentials in real time during authentication while keeping them stored in the U2F
33
+ format, and in a backfill task to store credentials in the new format, depending on how you are approaching your
34
+ migration.
35
+
36
+ ```ruby
37
+ require "webauthn/u2f_migrator"
38
+
39
+ migrated_credential = WebAuthn::U2fMigrator.new(
40
+ app_id: domain,
41
+ certificate: u2f_registration.certificate,
42
+ key_handle: u2f_registration.key_handle,
43
+ public_key: u2f_registration.public_key,
44
+ counter: u2f_registration.counter
45
+ )
46
+ migrated_credential.credential.id
47
+ # => "\x99\xB5LE83I>q.\xE9\x9C\x90l\xED'\xD5E[\xAB\xDE9\xB7\xCD!\x85\x92\x9F{\x13\xA8\x86"
48
+ migrated_credential.credential.public_key
49
+ # => "\xA5\x03& \x01!X \xE2P^Q`\xF9\x97\xD9*n<\x14\xDA\xB6a\xEEoK\x03\xACpMb\xED\x8B\x06E\"#!\xED\xC6\x01\x02\"X #C\x97\xAD C\x000\xE7\xD1\xD4%\xCFh\x83\xCD\x9E\xCB\xBC,\"\x1F>\xF6SZ\xA1U\xAB7\xBE\xEB"
50
+ migrated_credential.authenticator_data.sign_count
51
+ # => 41
52
+ ```
53
+
54
+ ## Authenticate migrated U2F credentials
55
+
56
+ Following the documentation on the [authentication initiation](https://github.com/cedarcode/webauthn-ruby/blob/master/README.md#initiation-phase-1),
57
+ you need to specify the [FIDO AppID extension](https://www.w3.org/TR/webauthn/#sctn-appid-extension) for U2F migratedq
58
+ credentials. The WebAuthn standard explains:
59
+
60
+ > The FIDO APIs use an alternative identifier for Relying Parties called an _AppID_, and any credentials created using
61
+ > those APIs will be scoped to that identifier. Without this extension, they would need to be re-registered in order to
62
+ > be scoped to an RP ID.
63
+
64
+ For the earlier given example `domain` this means:
65
+ - FIDO AppID: `https://login.example.com`
66
+ - Valid RP IDs: `login.example.com` (default) and `example.com`
67
+
68
+ You can request the use of the `appid` extension by setting the AppID in the configuration, like this:
69
+
70
+ ```ruby
71
+ WebAuthn.configure do |config|
72
+ config.legacy_u2f_appid = "https://login.example.com"
73
+ end
74
+ ```
75
+
76
+ By doing this, the `appid` extension will be automatically requested when generating the options for get:
77
+
78
+ ```ruby
79
+ options = WebAuthn::Credential.options_for_get
80
+ ```
81
+
82
+ On the frontend, in the resolved value from `navigator.credentials.get({ "publicKey": credentialRequestOptions })` add
83
+ a call to [getClientExtensionResults()](https://www.w3.org/TR/webauthn/#dom-publickeycredential-getclientextensionresults)
84
+ and send its result to your backend alongside the `id`/`rawId` and `response` values. If the authenticator used the AppID
85
+ extension, the returned value will contain `{ "appid": true }`.
86
+
87
+ During authentication verification phase, if you followed the [verification phase documentation](https://github.com/cedarcode/webauthn-ruby#verification-phase-1) and have set the AppID in the config, the method `PublicKeyCredentialWithAssertion#verify` will be smart enough to determine if it should use the AppID or the RP ID to verify the WebAuthn credential, depending on the output of the `appid` client extension:
88
+
89
+ > If true, the AppID was used and thus, when verifying an assertion, the Relying Party MUST expect the `rpIdHash` to be
90
+ > the hash of the _AppID_, not the RP ID.
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cbor"
4
+ require "forwardable"
5
+ require "openssl"
6
+ require "webauthn/attestation_statement"
7
+ require "webauthn/authenticator_data"
8
+
9
+ module WebAuthn
10
+ class AttestationObject
11
+ extend Forwardable
12
+
13
+ def self.deserialize(attestation_object, relying_party)
14
+ from_map(CBOR.decode(attestation_object), relying_party)
15
+ end
16
+
17
+ def self.from_map(map, relying_party)
18
+ new(
19
+ authenticator_data: WebAuthn::AuthenticatorData.deserialize(map["authData"]),
20
+ attestation_statement: WebAuthn::AttestationStatement.from(
21
+ map["fmt"],
22
+ map["attStmt"],
23
+ relying_party: relying_party
24
+ )
25
+ )
26
+ end
27
+
28
+ attr_reader :authenticator_data, :attestation_statement, :relying_party
29
+
30
+ def initialize(authenticator_data:, attestation_statement:)
31
+ @authenticator_data = authenticator_data
32
+ @attestation_statement = attestation_statement
33
+ end
34
+
35
+ def valid_attested_credential?
36
+ authenticator_data.attested_credential_data_included? &&
37
+ authenticator_data.attested_credential_data.valid?
38
+ end
39
+
40
+ def valid_attestation_statement?(client_data_hash)
41
+ attestation_statement.valid?(authenticator_data, client_data_hash)
42
+ end
43
+
44
+ def_delegators :authenticator_data, :credential, :aaguid
45
+ def_delegators :attestation_statement, :attestation_certificate_key_id
46
+ end
47
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "webauthn/attestation_statement/base"
5
+
6
+ module WebAuthn
7
+ module AttestationStatement
8
+ class Apple < Base
9
+ # Source: https://www.apple.com/certificateauthority/private/
10
+ ROOT_CERTIFICATE =
11
+ OpenSSL::X509::Certificate.new(<<~PEM)
12
+ -----BEGIN CERTIFICATE-----
13
+ MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
14
+ HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
15
+ bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
16
+ NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
17
+ A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
18
+ AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
19
+ xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
20
+ pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
21
+ 2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
22
+ MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
23
+ jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
24
+ 1bWeT0vT
25
+ -----END CERTIFICATE-----
26
+ PEM
27
+
28
+ NONCE_EXTENSION_OID = "1.2.840.113635.100.8.2"
29
+
30
+ def valid?(authenticator_data, client_data_hash)
31
+ valid_nonce?(authenticator_data, client_data_hash) &&
32
+ matching_public_key?(authenticator_data) &&
33
+ trustworthy? &&
34
+ [attestation_type, attestation_trust_path]
35
+ end
36
+
37
+ private
38
+
39
+ def valid_nonce?(authenticator_data, client_data_hash)
40
+ extension = cred_cert&.find_extension(NONCE_EXTENSION_OID)
41
+
42
+ if extension
43
+ sequence = OpenSSL::ASN1.decode(extension.value_der)
44
+
45
+ sequence.tag == OpenSSL::ASN1::SEQUENCE &&
46
+ sequence.value.size == 1 &&
47
+ sequence.value[0].value[0].value ==
48
+ OpenSSL::Digest::SHA256.digest(authenticator_data.data + client_data_hash)
49
+ end
50
+ end
51
+
52
+ def attestation_type
53
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_ANONCA
54
+ end
55
+
56
+ def cred_cert
57
+ attestation_certificate
58
+ end
59
+
60
+ def default_root_certificates
61
+ [ROOT_CERTIFICATE]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cose"
4
+ require "openssl"
5
+ require "webauthn/authenticator_data/attested_credential_data"
6
+ require "webauthn/error"
7
+
8
+ module WebAuthn
9
+ module AttestationStatement
10
+ class UnsupportedAlgorithm < Error; end
11
+
12
+ ATTESTATION_TYPE_NONE = "None"
13
+ ATTESTATION_TYPE_BASIC = "Basic"
14
+ ATTESTATION_TYPE_SELF = "Self"
15
+ ATTESTATION_TYPE_ATTCA = "AttCA"
16
+ ATTESTATION_TYPE_BASIC_OR_ATTCA = "Basic_or_AttCA"
17
+ ATTESTATION_TYPE_ANONCA = "AnonCA"
18
+
19
+ ATTESTATION_TYPES_WITH_ROOT = [
20
+ ATTESTATION_TYPE_BASIC,
21
+ ATTESTATION_TYPE_BASIC_OR_ATTCA,
22
+ ATTESTATION_TYPE_ATTCA,
23
+ ATTESTATION_TYPE_ANONCA
24
+ ].freeze
25
+
26
+ class Base
27
+ AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4"
28
+
29
+ def initialize(statement, relying_party = WebAuthn.configuration.relying_party)
30
+ @statement = statement
31
+ @relying_party = relying_party
32
+ end
33
+
34
+ def valid?(_authenticator_data, _client_data_hash)
35
+ raise NotImplementedError
36
+ end
37
+
38
+ def format
39
+ WebAuthn::AttestationStatement::FORMAT_TO_CLASS.key(self.class)
40
+ end
41
+
42
+ def attestation_certificate
43
+ certificates&.first
44
+ end
45
+
46
+ def attestation_certificate_key_id
47
+ attestation_certificate.subject_key_identifier&.unpack1("H*")
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :statement, :relying_party
53
+
54
+ def matching_aaguid?(attested_credential_data_aaguid)
55
+ extension = attestation_certificate&.find_extension(AAGUID_EXTENSION_OID)
56
+ if extension
57
+ aaguid_value = OpenSSL::ASN1.decode(extension.value_der).value
58
+ aaguid_value == attested_credential_data_aaguid
59
+ else
60
+ true
61
+ end
62
+ end
63
+
64
+ def matching_public_key?(authenticator_data)
65
+ attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der
66
+ end
67
+
68
+ def certificates
69
+ @certificates ||=
70
+ raw_certificates&.map do |raw_certificate|
71
+ OpenSSL::X509::Certificate.new(raw_certificate)
72
+ end
73
+ end
74
+
75
+ def algorithm
76
+ statement["alg"]
77
+ end
78
+
79
+ def raw_certificates
80
+ statement["x5c"]
81
+ end
82
+
83
+ def signature
84
+ statement["sig"]
85
+ end
86
+
87
+ def attestation_trust_path
88
+ if certificates&.any?
89
+ certificates
90
+ end
91
+ end
92
+
93
+ def trustworthy?(aaguid: nil, attestation_certificate_key_id: nil)
94
+ if ATTESTATION_TYPES_WITH_ROOT.include?(attestation_type)
95
+ relying_party.acceptable_attestation_types.include?(attestation_type) &&
96
+ valid_certificate_chain?(aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id)
97
+ else
98
+ relying_party.acceptable_attestation_types.include?(attestation_type)
99
+ end
100
+ end
101
+
102
+ def valid_certificate_chain?(aaguid: nil, attestation_certificate_key_id: nil)
103
+ root_certificates = root_certificates(
104
+ aaguid: aaguid,
105
+ attestation_certificate_key_id: attestation_certificate_key_id
106
+ )
107
+
108
+ if certificates&.one? && root_certificates.include?(attestation_certificate)
109
+ return true
110
+ end
111
+
112
+ attestation_root_certificates_store(
113
+ aaguid: aaguid,
114
+ attestation_certificate_key_id: attestation_certificate_key_id
115
+ ).verify(attestation_certificate, attestation_trust_path)
116
+ end
117
+
118
+ def attestation_root_certificates_store(aaguid: nil, attestation_certificate_key_id: nil)
119
+ OpenSSL::X509::Store.new.tap do |store|
120
+ root_certificates(
121
+ aaguid: aaguid,
122
+ attestation_certificate_key_id: attestation_certificate_key_id
123
+ ).each do |cert|
124
+ store.add_cert(cert)
125
+ end
126
+ end
127
+ end
128
+
129
+ def root_certificates(aaguid: nil, attestation_certificate_key_id: nil)
130
+ root_certificates =
131
+ relying_party.attestation_root_certificates_finders.reduce([]) do |certs, finder|
132
+ if certs.empty?
133
+ finder.find(
134
+ attestation_format: format,
135
+ aaguid: aaguid,
136
+ attestation_certificate_key_id: attestation_certificate_key_id
137
+ ) || []
138
+ else
139
+ certs
140
+ end
141
+ end
142
+
143
+ if root_certificates.empty? && respond_to?(:default_root_certificates, true)
144
+ default_root_certificates
145
+ else
146
+ root_certificates
147
+ end
148
+ end
149
+
150
+ def valid_signature?(authenticator_data, client_data_hash, public_key = attestation_certificate.public_key)
151
+ raise("Incompatible algorithm and key") unless cose_algorithm.compatible_key?(public_key)
152
+
153
+ cose_algorithm.verify(
154
+ public_key,
155
+ signature,
156
+ verification_data(authenticator_data, client_data_hash)
157
+ )
158
+ rescue COSE::Error
159
+ false
160
+ end
161
+
162
+ def verification_data(authenticator_data, client_data_hash)
163
+ authenticator_data.data + client_data_hash
164
+ end
165
+
166
+ def cose_algorithm
167
+ @cose_algorithm ||=
168
+ COSE::Algorithm.find(algorithm).tap do |alg|
169
+ alg && relying_party.algorithms.include?(alg.name) ||
170
+ raise(UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}")
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cose"
4
+ require "webauthn/attestation_statement/base"
5
+
6
+ module WebAuthn
7
+ module AttestationStatement
8
+ class FidoU2f < Base
9
+ class PublicKey
10
+ COORDINATE_LENGTH = 32
11
+ UNCOMPRESSED_FORM_INDICATOR = "\x04"
12
+
13
+ def self.uncompressed_point?(data)
14
+ data.size &&
15
+ data.length == UNCOMPRESSED_FORM_INDICATOR.length + COORDINATE_LENGTH * 2 &&
16
+ data[0] == UNCOMPRESSED_FORM_INDICATOR
17
+ end
18
+
19
+ def initialize(data)
20
+ @data = data
21
+ end
22
+
23
+ def valid?
24
+ data.size >= COORDINATE_LENGTH * 2 &&
25
+ cose_key.x.length == COORDINATE_LENGTH &&
26
+ cose_key.y.length == COORDINATE_LENGTH &&
27
+ cose_key.alg == COSE::Algorithm.by_name("ES256").id
28
+ end
29
+
30
+ def to_uncompressed_point
31
+ UNCOMPRESSED_FORM_INDICATOR + cose_key.x + cose_key.y
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :data
37
+
38
+ def cose_key
39
+ @cose_key ||= COSE::Key::EC2.deserialize(data)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cose"
4
+ require "openssl"
5
+ require "webauthn/attestation_statement/base"
6
+ require "webauthn/attestation_statement/fido_u2f/public_key"
7
+
8
+ module WebAuthn
9
+ module AttestationStatement
10
+ class FidoU2f < Base
11
+ VALID_ATTESTATION_CERTIFICATE_COUNT = 1
12
+ VALID_ATTESTATION_CERTIFICATE_ALGORITHM = COSE::Algorithm.by_name("ES256")
13
+ VALID_ATTESTATION_CERTIFICATE_KEY_CURVE = COSE::Key::Curve.by_name("P-256")
14
+
15
+ def valid?(authenticator_data, client_data_hash)
16
+ valid_format? &&
17
+ valid_certificate_public_key? &&
18
+ valid_credential_public_key?(authenticator_data.credential.public_key) &&
19
+ valid_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
20
+ valid_signature?(authenticator_data, client_data_hash) &&
21
+ trustworthy?(attestation_certificate_key_id: attestation_certificate_key_id) &&
22
+ [attestation_type, attestation_trust_path]
23
+ end
24
+
25
+ private
26
+
27
+ def valid_format?
28
+ !!(raw_certificates && signature) &&
29
+ raw_certificates.length == VALID_ATTESTATION_CERTIFICATE_COUNT
30
+ end
31
+
32
+ def valid_certificate_public_key?
33
+ certificate_public_key.is_a?(OpenSSL::PKey::EC) &&
34
+ certificate_public_key.group.curve_name == VALID_ATTESTATION_CERTIFICATE_KEY_CURVE.pkey_name &&
35
+ certificate_public_key.check_key
36
+ end
37
+
38
+ def valid_credential_public_key?(public_key_bytes)
39
+ public_key_u2f(public_key_bytes).valid?
40
+ end
41
+
42
+ def certificate_public_key
43
+ attestation_certificate.public_key
44
+ end
45
+
46
+ def valid_aaguid?(attested_credential_data_aaguid)
47
+ attested_credential_data_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID
48
+ end
49
+
50
+ def algorithm
51
+ VALID_ATTESTATION_CERTIFICATE_ALGORITHM.id
52
+ end
53
+
54
+ def verification_data(authenticator_data, client_data_hash)
55
+ "\x00" +
56
+ authenticator_data.rp_id_hash +
57
+ client_data_hash +
58
+ authenticator_data.credential.id +
59
+ public_key_u2f(authenticator_data.credential.public_key).to_uncompressed_point
60
+ end
61
+
62
+ def public_key_u2f(cose_key_data)
63
+ PublicKey.new(cose_key_data)
64
+ end
65
+
66
+ def attestation_type
67
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA
68
+ end
69
+ end
70
+ end
71
+ end