Devicecheck

Usage

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 and establishing your app's integrity.

Attestation

The mobile client must provide the following parameters:

  1. Key ID, encoded in "strict" Base64 format;
  2. Attestation Object, encoded in "strict" Base64;
  3. Challenge - unique one time challenge that must be provided by the server first.

Initialize the attestation service by providing your app ID and which environment are you testing.

attestation = Devicecheck::Attestation.new(
  app_id: 'com.example.random-app',
  environment: :production)

Call the attest method, passing the three parameters listed above.

pkey, receipt = attestation.attest(key_id:, attestation_object:, challenge:)

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. You must store both the public key and receipts associated with the unique app that you are attesting.

On the app side, the challenge needs to be SHA256-hashed and it is passed as the clientDataHash of the attestKey function.

Assertion

From the Apple docs:

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.

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.

assertion = Devicecheck::Assertion.new(
  app_id: 'com.example.random-app', 
  pkey_der: <some-key>)

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.

For example, if client_data is:

{ "new_score": 100 }

Then it must embed the challenge into this data, for example:

{ "new_score": 100, "challenge": "..." }

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.

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.

The endpoint will then call this library with the following parameters:

  1. Client Data (augmented with the challenge);
  2. Client Data challenge - copy of the challenge that is embedded in Client Data;
  3. Assertion Object;
  4. Expected challenge - the challenge that was previously sent, to be compared with what was provided by the client;
  5. Current assertion counter associated with this app - 0 if this is the first assertion.

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.

assertion.assert(client_data:,
                 client_data_challenge:,
                 expected_challenge:,
                 assertion_object:,
                 count: 0)

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.