devicecheck 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f390b2f4f12838ea2fdcda13f66b7921908d7abee05345924d7d69dd64181b97
4
+ data.tar.gz: 9faa3df7dd33770d1dca907ffa41d38ab31d38964d826a067ca9ddb6e15ee5e0
5
+ SHA512:
6
+ metadata.gz: 36d6ef82465760fee953e0b4d626b495f3b487428be068101a99f1c2bcb41d99575f8a45517848a61dd9ac00158ebb4e871b4d72ac331bbf659d2410ff4b559c
7
+ data.tar.gz: f9aeaed2c53257775775c03e10872af43b4c829fb09e99151abe66285f7555dc53946e783f242eb0c6a5713c8f64f1806b1e0584838895070996c888af78cf0f
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,29 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ require:
4
+ - rubocop-rake
5
+ - rubocop-rspec
6
+ - rubocop-performance
7
+ - rubocop-thread_safety
8
+
9
+ AllCops:
10
+ NewCops: enable
11
+ TargetRubyVersion: 3.2
12
+
13
+ Style/StringLiterals:
14
+ Enabled: true
15
+ EnforcedStyle: single_quotes
16
+
17
+ Style/StringLiteralsInInterpolation:
18
+ Enabled: true
19
+ EnforcedStyle: single_quotes
20
+
21
+ Layout/LineLength:
22
+ Max: 120
23
+
24
+ Style/Documentation:
25
+ Enabled: false
26
+
27
+ Metrics/BlockLength:
28
+ Exclude:
29
+ - spec/**/*
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,27 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2024-03-18 07:21:43 UTC using RuboCop version 1.62.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 23
10
+ # Configuration parameters: AllowSubject.
11
+ RSpec/MultipleMemoizedHelpers:
12
+ Max: 24
13
+
14
+ # Offense count: 21
15
+ # Configuration parameters: EnforcedStyle, IgnoreSharedExamples.
16
+ # SupportedStyles: always, named_only
17
+ RSpec/NamedSubject:
18
+ Exclude:
19
+ - 'spec/devicecheck/assertion_spec.rb'
20
+ - 'spec/devicecheck/attestation_spec.rb'
21
+ - 'spec/devicecheck/data/authenticator_data_spec.rb'
22
+ - 'spec/devicecheck/validators/certificate_chain_validator_spec.rb'
23
+
24
+ # Offense count: 7
25
+ # Configuration parameters: AllowedGroups.
26
+ RSpec/NestedGroups:
27
+ Max: 5
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ -mmarkdown
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## [1.0.0] - 2024-07-16
2
+
3
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,105 @@
1
+ # Contributing
2
+
3
+ We love pull requests from everyone. By participating in this project,
4
+ you agree to abide by the our [code of conduct](code_of_conduct).
5
+
6
+ Here are some ways *you* can contribute:
7
+
8
+ * by using alpha, beta, and prerelease versions
9
+ * by reporting bugs
10
+ * by suggesting new features
11
+ * by writing or editing documentation
12
+ * by writing specifications
13
+ * by writing code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace)
14
+ * by refactoring code
15
+ * by closing [issues]
16
+ * by reviewing patches
17
+
18
+ ## Submitting an Issue
19
+
20
+ * We use the [GitHub issue tracker][issues] to track bugs and features.
21
+ * Before submitting a bug report or feature request, check to make sure it hasn't
22
+ already been submitted.
23
+
24
+ * When submitting a bug report, please include a gist that includes a
25
+ stack trace and any details that may be necessary to reproduce the
26
+ bug, including your gem version, Ruby version, and operating system.
27
+ Ideally, a bug report should include a pull request with failing
28
+ specs.
29
+
30
+ ## Cleaning up issues
31
+
32
+ * Issues that have no response from the submitter will be closed after
33
+ 30 days.
34
+ * Issues will be closed once they're assumed to be fixed or
35
+ answered. If the maintainer is wrong, it can be opened again.
36
+ * If your issue is closed by mistake, please understand and explain the issue.
37
+ We will happily reopen the issue.
38
+
39
+ ## Submitting a Pull Request
40
+
41
+ 1. [Fork][fork] the [official repository][repo].
42
+ 1. [Create a topic branch.][branch]
43
+ 1. Implement your feature or bug fix.
44
+ 1. Add, commit, and push your changes.
45
+ 1. [Submit a pull request.][pr]
46
+
47
+ ### Notes
48
+
49
+ * Please add tests if you changed code. Contributions without tests
50
+ won't be accepted.
51
+ * If you don't know how to add tests, please put in a PR and leave a
52
+ comment asking for help. We love helping!
53
+ * Please don't update the Gem version.
54
+
55
+ ## Setting Up
56
+
57
+ After checking out the repo, run `bundle install` to install
58
+ dependencies. Then, run `rake spec` to run the tests. You can also
59
+ run `bin/console` for an interactive prompt that will allow you to
60
+ experiment.
61
+
62
+ To install this gem onto your local machine, run `bundle exec rake
63
+ install`.
64
+
65
+ ## Running the test suite
66
+
67
+ The default rake task will run the full test suite and lint:
68
+
69
+ ```sh
70
+ bundle exec rake
71
+ ```
72
+
73
+ To run an individual rspec test, you can provide a path and line number:
74
+
75
+ ```sh
76
+ bundle exec rspec spec/path/to/spec.rb:123
77
+ ```
78
+
79
+ ## Formatting and Style
80
+
81
+ Our style guide is defined in `.rubocop.yml`.
82
+
83
+ To run the linter:
84
+
85
+ ```sh
86
+ bundle exec rubocop
87
+ ```
88
+
89
+ To run the linter with auto correct:
90
+
91
+ ```sh
92
+ bundle exec rubocop -A
93
+ ```
94
+
95
+ Inspired by [factory_bot] and [activeinteractor].
96
+
97
+ [code_of_conduct]: CODE-OF-CONDUCT.md
98
+ [repo]: https://github.com/catawiki/devicecheck-ruby/tree/main
99
+ [issues]: https://github.com/catawiki/devicecheck-ruby/issues
100
+ [fork]: https://help.github.com/articles/fork-a-repo/
101
+ [branch]: https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/
102
+ [pr]: https://help.github.com/articles/using-pull-requests/
103
+ [gist]: https://gist.github.com/
104
+ [factory_bot]: https://github.com/thoughtbot/factory_bot/blob/master/CONTRIBUTING.md
105
+ [activeinteractor]: https://github.com/aaronmallen/activeinteractor/tree/main/CONTRIBUTING.md
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in devicecheck.gemspec
6
+ gemspec
7
+
8
+ gem 'rake'
9
+ gem 'rspec'
10
+ gem 'rubocop'
11
+ gem 'rubocop-performance'
12
+ gem 'rubocop-rake'
13
+ gem 'rubocop-rspec'
14
+ gem 'rubocop-thread_safety'
15
+ gem 'simplecov'
16
+ gem 'yard'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Catawiki B.V.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # Devicecheck
2
+
3
+ ## Usage
4
+
5
+ This gem provides the core functionality to verify attestations and assertions as generated by Apple's Devicecheck system. You should be familiar with how to [validate apps on the server side](https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server) and [establishing your app's integrity](https://developer.apple.com/documentation/devicecheck/establishing-your-app-s-integrity).
6
+
7
+ ### Attestation
8
+
9
+ The mobile client must provide the following parameters:
10
+
11
+ 1. Key ID, encoded in "strict" Base64 format;
12
+ 2. Attestation Object, encoded in "strict" Base64;
13
+ 3. Challenge - unique one time challenge that must be provided by the server first.
14
+
15
+ Initialize the attestation service by providing your app ID and which environment are you testing.
16
+
17
+ ```ruby
18
+ attestation = Devicecheck::Attestation.new(
19
+ app_id: 'com.example.random-app',
20
+ environment: :production)
21
+ ```
22
+
23
+ Call the `attest` method, passing the three parameters listed above.
24
+
25
+ ```
26
+ pkey, receipt = attestation.attest(key_id:, attestation_object:, challenge:)
27
+ ```
28
+
29
+ If there are issues with the provided parameters, a `RuntimeError` with the problem found will be raised. Otherwise the function returns both the public key of the credential data, DER-encoded and a receipt. The receipt can be later used to [check for fraud](https://developer.apple.com/documentation/devicecheck/assessing-fraud-risk). You must store both the public key and receipts associated with the unique app that you are attesting.
30
+
31
+ On the app side, the `challenge` needs to be SHA256-hashed and it is passed as the `clientDataHash` of the `attestKey` function.
32
+
33
+ ### Assertion
34
+
35
+ From the Apple docs:
36
+
37
+ > After successful attestation, your server can require the associated client to accompany server requests with an assertion object. Each verified assertion reestablishes the legitimacy of the client. You typically require this for requests that access sensitive or premium content.
38
+
39
+ Initialize the assertion service by providing both the App ID and the DER-encoded key that is associated with that App instance that was previously saved after it was attested.
40
+
41
+ ```ruby
42
+ assertion = Devicecheck::Assertion.new(
43
+ app_id: 'com.example.random-app',
44
+ pkey_der: <some-key>)
45
+ ```
46
+
47
+ Before calling an endpoint with sensitive data, the app must obtain a one time unique value from the server, which will call `challenge`. Then, it will compute an assertion by embedding this challenge into the `client_data` that will be sent to the endpoint later on.
48
+
49
+ For example, if `client_data` is:
50
+
51
+ ```
52
+ { "new_score": 100 }
53
+ ```
54
+
55
+ Then it must embed the challenge into this data, for example:
56
+
57
+ ```
58
+ { "new_score": 100, "challenge": "..." }
59
+ ```
60
+
61
+ Note that using JSON is just an example. The client data can simply be a string that is augmented with the challenge. It depends on the use case and the interface must be established between the mobile app and the server.
62
+
63
+ Then, when calling the protected endpoint, the app must provide, along with the augmented client data, the assertion generated by the `generateAssertion` function from the Devicecheck service from Apple.
64
+
65
+ The endpoint will then call this library with the following parameters:
66
+
67
+ 1. Client Data (augmented with the challenge);
68
+ 1. Client Data challenge - copy of the challenge that is embedded in Client Data;
69
+ 1. Assertion Object;
70
+ 1. Expected challenge - the challenge that was previously sent, to be compared with what was provided by the client;
71
+ 1. Current assertion counter associated with this app - 0 if this is the first assertion.
72
+
73
+ We explictily request both `Client Data` and `Client Data challenge` because this library makes no assumption of the format of `Client Data` and would not know how to extract it otherwise.
74
+
75
+ ```ruby
76
+ assertion.assert(client_data:,
77
+ client_data_challenge:,
78
+ expected_challenge:,
79
+ assertion_object:,
80
+ count: 0)
81
+ ```
82
+
83
+ If the assertion is valid, the function will return the count of assertions so far -- this must be stored associated with the app. Later invocations of this method must provide the current count.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yard'
4
+ require 'bundler/gem_tasks'
5
+ require 'rspec/core/rake_task'
6
+ require 'rubocop/rake_task'
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+ RuboCop::RakeTask.new
10
+ YARD::Rake::YardocTask.new
11
+
12
+ task default: %i[spec rubocop yard]
data/SECURITY.md ADDED
@@ -0,0 +1,24 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ | ------- | ------------------------ |
7
+ | 1.0.0 | :rocket: |
8
+
9
+ - :rocket: - Currently addressing feature requests
10
+ - :beetle: - Currently addressing bug reports
11
+ - :lock: - Currently addressing security reports
12
+ - :x: - No longer supported
13
+
14
+ ## Reporting a Vulnerability
15
+
16
+ To report a security vulnerability please create a security report on the [GitHub issue tracker][issues] tracker.
17
+ Security issues will be triaged as soon as possible. We also welcome pull requests, please see our
18
+ [contributing guidelines][contributing] for more information on how to contribute.
19
+
20
+ Inspired by [activeinteractor]
21
+
22
+ [issues]: https://github.com/catawiki/devicecheck-ruby/issues
23
+ [contributing]: https://github.com/catawiki/devicecheck-ruby/blob/master/CONTRIBUTING.md
24
+ [activeinteractor]: https://github.com/aaronmallen/activeinteractor
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/devicecheck/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'devicecheck'
7
+ spec.version = Devicecheck::VERSION
8
+ spec.authors = ['Fabricio Chalub']
9
+ spec.email = ['opensource@catawiki.nl']
10
+ spec.license = 'MIT'
11
+ spec.homepage = 'https://github.com/catawiki/devicecheck-ruby'
12
+ spec.description = 'Pure Ruby implementation of the Apple App Attestation server side verifier'
13
+ spec.summary = 'Apple App Attestation (aka DeviceCheck) support for Ruby.'
14
+ spec.required_ruby_version = '>= 3.2'
15
+
16
+ spec.metadata = {
17
+ 'bug_tracker_uri' => 'https://github.com/catawiki/devicecheck-ruby/issues',
18
+ 'changelog_uri' => 'https://github.com/catawiki/devicecheck-ruby/CHANGELOG.md',
19
+ 'documentation_uri' => 'https://rubydoc.info/github/catawiki/devicecheck-ruby',
20
+ 'source_code_uri' => 'https://github.com/catawiki/devicecheck-ruby',
21
+ 'rubygems_mfa_required' => 'true'
22
+ }
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(__dir__) do
27
+ `git ls-files -z`.split("\x0").reject do |f|
28
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
29
+ end
30
+ end
31
+ spec.bindir = 'exe'
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ['lib']
34
+
35
+ spec.add_dependency 'base64', '~> 0.2.0'
36
+ spec.add_dependency 'cbor', '~> 0.5.9'
37
+ spec.add_dependency 'openssl', '~> 3'
38
+
39
+ # For more information and examples about making a new gem, check out our
40
+ # guide at: https://bundler.io/guides/creating_gem.html
41
+ spec.metadata['rubygems_mfa_required'] = 'true'
42
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 Catawiki B.V.
4
+ #
5
+ # Licensed under the MIT License (the "License");
6
+ #
7
+ # you may not use this file except in compliance with the License.
8
+ #
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # https://opensource.org/licenses/MIT
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ module Devicecheck
20
+ class Assertion
21
+ # Initialize the assertion service by providing both the App ID
22
+ # and the DER-encoded key that is associated with that App
23
+ # instance that was previously saved after it was attested.
24
+ def initialize(app_id:, pkey_der:)
25
+ @app_id = app_id
26
+ @pkey = OpenSSL::PKey.read(pkey_der)
27
+ @sha256 = OpenSSL::Digest.new('SHA256')
28
+ end
29
+
30
+ # Verifies an assertion generated by the `generateAssertion`
31
+ # method from DCAppAttestService.
32
+ #
33
+ # The app must obtain a one time unique value from the server,
34
+ # which we will call `challenge`. Then, it will compute an
35
+ # assertion by embedding this challenge into the `client_data`.
36
+ #
37
+ # For example, if `client_data` is:
38
+ #
39
+ # ```
40
+ # { "new_score": 100 }
41
+ # ```
42
+ #
43
+ # Then it must embed the challenge into this data, for example:
44
+ #
45
+ # ```
46
+ # { "new_score": 100, "challenge": "..." }
47
+ # ```
48
+ #
49
+ # Note that using JSON strings is just an example. It depends on
50
+ # the use case and the interface must be established between the
51
+ # mobile app and the server. This is why we expect another
52
+ # parameter (`client_data_challenge`) that contains the embedded
53
+ # challenge value, since this library does not make any
54
+ # assumption on the format or contents of `client_data`.
55
+ #
56
+ # @param client_data [String] client data (with embedded
57
+ # challenge)
58
+ # @param client_data_challenge [String] client data challenge -
59
+ # copy of the challenge that is embedded in client data
60
+ # @param assertion_object [String] Base64-encoded assertion
61
+ # object
62
+ # @param expected_challenge [String] the challenge that was
63
+ # previously sent, to be compared with what was provided by the
64
+ # client
65
+ # @param count [Integer] current assertion counter associated with
66
+ # this app - 0 if this is the first assertion
67
+ #
68
+ # @return [Integer] the authenticator `counter` value, to be stored for later use
69
+ def assert(client_data:, client_data_challenge:, expected_challenge:, assertion_object:, count: 0)
70
+ decoded_assertion_object = CBOR.decode(Base64.strict_decode64(assertion_object))
71
+
72
+ signature = decoded_assertion_object['signature']
73
+ authenticator_data = decoded_assertion_object['authenticatorData']
74
+
75
+ (rp_id_hash, _, sign_count,) = Data::AuthenticatorData.unpack(authenticator_data)
76
+
77
+ validate_signature!(signature, client_data, authenticator_data)
78
+
79
+ validate_rp_id!(rp_id_hash)
80
+
81
+ raise 'Failed count check' if sign_count < count
82
+ raise 'Failed challenge check' unless client_data_challenge == expected_challenge
83
+
84
+ sign_count
85
+ end
86
+
87
+ private
88
+
89
+ attr_reader :app_id, :pkey, :sha256
90
+
91
+ def validate_signature!(signature, client_data, authenticator_data)
92
+ client_data_hash = sha256.digest(client_data)
93
+
94
+ nonce = sha256.digest(authenticator_data + client_data_hash)
95
+
96
+ raise 'Failed signature check' unless pkey.verify(sha256, signature, nonce)
97
+ end
98
+
99
+ def validate_rp_id!(rp_id_hash)
100
+ raise 'Failed RP ID check' unless rp_id_hash == sha256.digest(app_id)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 Catawiki B.V.
4
+ #
5
+ # Licensed under the MIT License (the "License");
6
+ #
7
+ # you may not use this file except in compliance with the License.
8
+ #
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # https://opensource.org/licenses/MIT
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'base64'
20
+ require 'cbor'
21
+ require 'json'
22
+ require 'openssl'
23
+ require 'securerandom'
24
+
25
+ module Devicecheck
26
+ class Attestation
27
+ ##
28
+ # AAGUID for development environments
29
+ AAGUID_DEVELOPMENT = 'appattestdevelop'
30
+ # AAGUID for production environments
31
+ AAGUID_PRODUCTION = "appattest\0\0\0\0\0\0\0"
32
+
33
+ #
34
+ # Initialize the attestation service by providing your app ID and
35
+ # which environment are you testing.
36
+ #
37
+ # @param app_id [String] your App ID
38
+ # @param environment [Symbol] `:production` or `:development`
39
+ def initialize(app_id:, environment:)
40
+ @app_id = app_id
41
+ @environment = environment
42
+ @sha256 = OpenSSL::Digest.new('SHA256')
43
+ end
44
+
45
+ #
46
+ # Verifies the attestation generated by DCAppAttestService. All
47
+ # Base64 encoded strings should be sent in strict format (RFC 4648).
48
+ #
49
+ # @param key_id [String] Base64-encoded format of the public key ID
50
+ # @param attestation_object [String] Base64-encoded of the
51
+ # attestation object generated by the `attestKey` method of
52
+ # `DCAppAttestService`
53
+ # @param challenge [String] challenge originally provided by the server
54
+ # @return [Array] An array containing:
55
+ # - the verified public key in DER format
56
+ # - the receipt from the attestation statement, which can be used
57
+ # later to request a fraud assessment metric from Apple.
58
+ #
59
+ # If the key cannot be verified, a runtime error will be raised
60
+ # containing details about the failed check.
61
+ def attest(key_id:, attestation_object:, challenge:)
62
+ decoded_attestation_object = CBOR.decode(Base64.strict_decode64(attestation_object))
63
+
64
+ att_stmt = decoded_attestation_object['attStmt']
65
+ auth_data = decoded_attestation_object['authData']
66
+
67
+ cred_cert = validate_certificates! att_stmt
68
+
69
+ validate_challenge! challenge, auth_data, cred_cert
70
+ validate_key_id! key_id, cred_cert
71
+ validate_auth_data! key_id, auth_data
72
+
73
+ [cred_cert.public_key.to_der, att_stmt['receipt']]
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :app_id, :environment, :sha256
79
+
80
+ def validate_certificates!(att_stmt)
81
+ cred_cert = Validators::CertificateChainValidator.validate(att_stmt)
82
+
83
+ raise 'Failed certificate chain check' unless cred_cert
84
+
85
+ cred_cert
86
+ end
87
+
88
+ def validate_challenge!(challenge, auth_data, cred_cert)
89
+ client_data_hash = sha256.digest(challenge)
90
+
91
+ sequence = OpenSSL::ASN1.decode(cred_cert.find_extension('1.2.840.113635.100.8.2').value_der)
92
+
93
+ unless valid_sequence?(sequence) &&
94
+ sequence.value[0].value[0].value ==
95
+ sha256.digest(auth_data + client_data_hash)
96
+ raise 'Failed challenge check'
97
+ end
98
+ end
99
+
100
+ def valid_sequence?(sequence)
101
+ sequence.tag == OpenSSL::ASN1::SEQUENCE &&
102
+ sequence.value.size == 1
103
+ end
104
+
105
+ def validate_key_id!(key_id, cred_cert)
106
+ uncompressed_point_key = cred_cert.public_key.public_key.to_octet_string(:uncompressed)
107
+
108
+ return if key_id == Base64.strict_encode64(sha256.digest(uncompressed_point_key))
109
+
110
+ raise 'Failed key ID check'
111
+ end
112
+
113
+ def validate_auth_data!(key_id, auth_data)
114
+ (rp_id_hash, _, sign_count, aaguid, credential_id) = Data::AuthenticatorData.unpack(auth_data)
115
+
116
+ raise 'Failed RP ID check' unless rp_id_hash == sha256.digest(app_id)
117
+ raise 'Failed sign counter = 0 check' unless sign_count.zero?
118
+ raise 'Failed AAGUID check' unless aaguid == expected_aaguid
119
+ raise 'Failed credentialId check' unless key_id == Base64.strict_encode64(credential_id)
120
+ end
121
+
122
+ def expected_aaguid
123
+ case environment
124
+ when :production
125
+ AAGUID_PRODUCTION
126
+ when :development
127
+ AAGUID_DEVELOPMENT
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 Catawiki B.V.
4
+ #
5
+ # Licensed under the MIT License (the "License");
6
+ #
7
+ # you may not use this file except in compliance with the License.
8
+ #
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # https://opensource.org/licenses/MIT
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ module Devicecheck
20
+ module Data
21
+ # Unpacks authenticator data according to the [webauthn specification](https://www.w3.org/TR/webauthn-2/#authenticator-data).
22
+ #
23
+ # ```
24
+ # Authenticator data layout:
25
+ # -------------------------
26
+ #
27
+ # rp_id_hash = 32 bytes
28
+ # flags = 1 byte
29
+ # sign-count = 4 bytes
30
+ # aaguid = 16
31
+ # attestedCredentialData (variable)
32
+ # - aaguid = 16
33
+ # credentialIdLength(L) = 2
34
+ # credentialId = L
35
+ # credentialPublicKey = variable
36
+ # extension (variable)
37
+ # ```
38
+ class AuthenticatorData
39
+ def self.unpack(...) = new.unpack(...)
40
+
41
+ def unpack(auth_data)
42
+ (rp_id_hash, flags, sign_count, trailing_bytes) =
43
+ auth_data.unpack('a32c1N1a*')
44
+
45
+ (aaguid, credential_id_length, trailing_bytes) =
46
+ trailing_bytes.unpack('a16na*')
47
+
48
+ (credential_id, credential_public_key) =
49
+ trailing_bytes.unpack("a#{credential_id_length}a*")
50
+
51
+ [rp_id_hash, flags, sign_count, aaguid,
52
+ credential_id, credential_public_key]
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 Catawiki B.V.
4
+ #
5
+ # Licensed under the MIT License (the "License");
6
+ #
7
+ # you may not use this file except in compliance with the License.
8
+ #
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # https://opensource.org/licenses/MIT
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ module Devicecheck
20
+ module Validators
21
+ # Verifies that the x5c array contains the intermediate and leaf
22
+ # certificates for App Attest, starting from the credential
23
+ # certificate in the first data buffer in the array
24
+ # (credcert). Uses Apple’s [App Attest root
25
+ # certificate](https://www.apple.com/certificateauthority/Apple_App_Attestation_Root_CA.pem).
26
+ class CertificateChainValidator
27
+ ROOT_CA =
28
+ OpenSSL::X509::Certificate.new(<<~PEM)
29
+ -----BEGIN CERTIFICATE-----
30
+ MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYw
31
+ JAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwK
32
+ QXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNa
33
+ Fw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlv
34
+ biBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9y
35
+ bmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdh
36
+ NbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9au
37
+ Yen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/
38
+ MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYw
39
+ CgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn
40
+ 53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijV
41
+ oyFraWVIyd/dganmrduC1bmTBGwD
42
+ -----END CERTIFICATE-----
43
+ PEM
44
+
45
+ def self.validate(...) = new.validate(...)
46
+
47
+ def initialize(root_ca: ROOT_CA)
48
+ @certificates_store = OpenSSL::X509::Store.new.add_cert(root_ca)
49
+ end
50
+
51
+ def validate(att_stmt)
52
+ certificates =
53
+ att_stmt['x5c'].map { |c| OpenSSL::X509::Certificate.new(c) }
54
+
55
+ cred_cert, *certificate_chain = certificates
56
+
57
+ store_context = OpenSSL::X509::StoreContext.new(
58
+ certificates_store, cred_cert, certificate_chain
59
+ )
60
+
61
+ cred_cert if store_context.verify
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :certificates_store
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devicecheck
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 Catawiki B.V.
4
+ #
5
+ # Licensed under the MIT License (the "License");
6
+ #
7
+ # you may not use this file except in compliance with the License.
8
+ #
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # https://opensource.org/licenses/MIT
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'openssl'
20
+ require 'base64'
21
+ require 'cbor'
22
+
23
+ require_relative 'devicecheck/version'
24
+ require_relative 'devicecheck/data/authenticator_data'
25
+ require_relative 'devicecheck/validators/certificate_chain_validator'
26
+ require_relative 'devicecheck/attestation'
27
+ require_relative 'devicecheck/assertion'
28
+
29
+ # {include:file:README.md}
30
+ module Devicecheck
31
+ end
@@ -0,0 +1,4 @@
1
+ module Devicecheck
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: devicecheck
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Fabricio Chalub
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-07-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: base64
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: cbor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.9
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.9
41
+ - !ruby/object:Gem::Dependency
42
+ name: openssl
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3'
55
+ description: Pure Ruby implementation of the Apple App Attestation server side verifier
56
+ email:
57
+ - opensource@catawiki.nl
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".rspec"
63
+ - ".rubocop.yml"
64
+ - ".rubocop_todo.yml"
65
+ - ".yardopts"
66
+ - CHANGELOG.md
67
+ - CODE-OF-CONDUCT.md
68
+ - CONTRIBUTING.md
69
+ - Gemfile
70
+ - LICENSE
71
+ - README.md
72
+ - Rakefile
73
+ - SECURITY.md
74
+ - devicecheck.gemspec
75
+ - lib/devicecheck.rb
76
+ - lib/devicecheck/assertion.rb
77
+ - lib/devicecheck/attestation.rb
78
+ - lib/devicecheck/data/authenticator_data.rb
79
+ - lib/devicecheck/validators/certificate_chain_validator.rb
80
+ - lib/devicecheck/version.rb
81
+ - sig/devicecheck.rbs
82
+ homepage: https://github.com/catawiki/devicecheck-ruby
83
+ licenses:
84
+ - MIT
85
+ metadata:
86
+ bug_tracker_uri: https://github.com/catawiki/devicecheck-ruby/issues
87
+ changelog_uri: https://github.com/catawiki/devicecheck-ruby/CHANGELOG.md
88
+ documentation_uri: https://rubydoc.info/github/catawiki/devicecheck-ruby
89
+ source_code_uri: https://github.com/catawiki/devicecheck-ruby
90
+ rubygems_mfa_required: 'true'
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '3.2'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.4.10
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Apple App Attestation (aka DeviceCheck) support for Ruby.
110
+ test_files: []