Implementing Apple's Device Check App Attest Protocol
Tags: ios, security, swift, csharp
Published: 2023-07-12 13:15:01

When creating a back-end for your App, you may want to secure your API so that you can ensure that the App cannot be used by agents other than legitimate APP installs running on legitimate devices. This lowers the API attack surface by limiting exposure to only legitimate APP installs.

As developers, APIs are often protected by user authentication and authorisation; but in instances where there is no user authentication your API is left unprotected.

Suggestions on many forums, that APIs can be protected by configuring your APP with a secret "API Key", are sub-optimal since it is essentially _"security by obfuscation"_ (the secret can be retrieved by extracting it from your APP package).

The answer to both, "how can I secure my API from illegitimate clients? ", and "how should I protect unauthenticated clients?", is **"App Attestation"**! 🎉🥳

Apple and Android have solutions for App Attestation, but this article will be focused on Apple's [Device Check App Attest](https://developer.apple.com/documentation/devicecheck/establishing_your_app_s_integrity).

## Basic Flow

The figure taken from the App Attest "Establishing Your App's Integrity" document shows there are three high-level components involved in App Attest;

 - your app,
 - your server,
 - and the app attest service.

There is a little more involved than that, and a little more that is useful to know about the high-level components.

![High-Level App Attest Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gkdxhkwwp7g1osm9p6gi.png "High-Level App Attest Architecture")

Critically, the "App Attest" service, as it is shown in the figure above, is an internet-exposed service, which may fail or timeout from time to time.

Furthermore, the [Secure Enclave](https://support.apple.com/en-au/guide/security/sec59b0b31ff/web) on the device is used to prevent "Your App" from direct access to the private key generated for the use of App Attest.

![Basic Protocol Flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/muijp0b0urbuzou7oo8z.png "Basic Protocol Flow")

1. Your server delivers a string of random bytes which represents a challenge to your app.
2. Your app creates a cryptographic key-pair using the device check API. The key resides in the device's "Secure Enclave" and the operation responds with a reference to that public/private key pair with an identifier string (the key ID string is a SHA256 hash of the public key).
3. A hash of the challenge data along with the key identifier is sent to the _Apple App Attest_ service over the internet. The service responds with the attestation data as a string of bytes.
4. The attestation data is provided to your server, and the server validates the attestation data.
5. Subsequence requests to your server are accompanied by assertion data which are generated on the device with the key identifier and the request body.
6. The assertion data are then validated and the request is processed.

Let's break down how each of these things work. Then iterate on the process to try and take into account HTTP standards for authentication and security practices.

### Warning: Check that App Attest is Available

A "step zero" not mentioned above is that you should check your App's platform [supports the App Attest service](https://developer.apple.com/documentation/devicecheck/establishing_your_app_s_integrity#3576028) before continuing the process.

``` swift
let service = DCAppAttestService.shared
if service.isSupported {
    // Perform key generation and attestation.
}
// Continue with server access.
```

The annoying fact about `DCAppAttestService` is that it doesn't work on the simulator. 😣

The documentation states it is available on the following platform versions:

| iOS | iPadOS | macOS | tvOS | watchOS |
| --- | ------ | ------| ---- | ------- |
|14.0+|14.0+   |11.0+. |15.0+ | 9.0+    |

### Generating the Challenge (.NET)

In response to some request from your app, your server responds with a string of random bytes, which are referred to as the "challenge".

In cases of security and cryptography, random is just not just _any_ PRNG, it _should_ be cryptographically random so that the sequence cannot be easily anticipated by an attacker.

In .NET, we can use the [`RandomNumberGenerator`](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.randomnumbergenerator?view=net-7.0) found in the `System.Security.Cryptography` namespace and we simply call `GetBytes(length)` on the static instance of the class to get a cryptographically random sequence of bytes at the desired length.

### Generating a Public/Private Key Pair and ID (Swift)

The Apple docs give detail on this, you will want to persist the key identifier response in a local cache or storage so it may be used later in the process.

The generated key pair and identifier are unique for each user account on each device running your app. So you only need to store one key identifier in your code.

The basic code for generating the key pair is:

``` swift
service.generateKey { keyId, error in
    guard error == nil else { /* Handle the error. */ }

    // Cache keyId for subsequent operations.
}
```

I prefer to use `async`/`await` rather than callbacks, so here it is refactored:

``` swift
public func getKeyId() async throws -> String {
    if let keyId = try _keyId.value {
        return keyId
    }
    
    return try await withCheckedThrowingContinuation { continuation in
        _service.generateKey { keyId, error in
            if let keyIdValue = keyId {
                do {
                    try self._keyId.set(value: keyIdValue)
                    continuation.resume(returning: keyIdValue)
                }
                catch {
                    continuation.resume(throwing: AppAttestError.GetKey(error))
                }
            }
            else {
                continuation.resume(throwing: AppAttestError.GetKey(error))
            }
        }
    }
}
```

The above code makes use of an instance of `self._keyId` to manage the reuse of a generated key pair. The instance is of a type that serves as an interface to the [device keychain secret storage](https://developer.apple.com/documentation/security/keychain_services), the implementation of that will be shown later.

It also uses `AppAttestError`, and again, I will share this code later.

1. Try and get the stored `keyId` value and return early.
2. Asynchronously generate the key pair and handle errors with either generation or persisting the key identifier
3. Capture the key identifier response (or error) with the continuation.

### Generate Attestation (Swift)

After retrieving the challenge and obtaining the key identifier we should generate the attestation data. Keep in mind this operation is made against an Apple ran web service; from my experimentation, this service frequently times out or is temporarily unavailable, so make sure to consider this in your design.

When generating the attestation data we need to make use of both the challenge we received earlier _and_ the key identifier.

Apple's instructions for implementing this step are shown under [Certify the key pairs as valid](https://developer.apple.com/documentation/devicecheck/establishing_your_app_s_integrity#3561588).

``` swift
import CryptoKit

let challenge = <# Data from your server. #>
let hash = Data(SHA256.hash(data: challenge))

service.attestKey(keyId, clientDataHash: hash) { attestation, error in
    guard error == nil else { /* Handle error and return. */ }

    // Send the attestation object to your server for verification.
}
```

As before, here is a sensible implementation of this function using `async`.

``` swift
public func generateAttestation(keyId: String, challenge: Data) async throws -> Data {
    let challengeHash = Data(SHA256.hash(data: challenge))
    
    return try await withCheckedThrowingContinuation { continuation in
        _service.attestKey(keyId, clientDataHash: challengeHash) { attestation, error in
            if let attestationValue = attestation {
                continuation.resume(returning: attestationValue)
            }
            else if let dcError = error as? DCError, dcError.code == .invalidKey {
                do {
                    try self._keyId.remove()
                    continuation.resume(throwing: AppAttestError.GenerateAttestation(error))
                }
                catch {
                    continuation.resume(throwing: AppAttestError.GenerateAttestation(error))
                }
            }
            else {
                continuation.resume(throwing: AppAttestError.GenerateAttestation(error))
            }
        }
    }
}
```

The above code handles the case of the "invalid key" App Attest error. In this scenario, we should invalidate the local key identifier cache so that it will get recreated.

### Validating Attestation Statement

The steps to validating the attestation statement are outlined [here](https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server#3576643).

This represents the bulk of the protocol implementation, there are lots of steps and lots of concepts to understand. Let's try and break it down to make it digestible.

#### High-Level Steps

1. Decode the attestation data
1. Verify statement certificates
2. Verify cryptographic nonce
3. Verify key identifier against statement certificate
4. Verify app identifier
5. Verify the signing counter
6. Verify development or product environment
7. Verify key identifier against credential identifier
8. Store attestation data for assertion data verification

... yikes 😅

I don't expect all of that to make sense at this point, so please continue reading...

#### The Attestation Object Structure

To decode the attestation statement we need to cover:
- concepts and structures detailed within the [Web Authentication](https://www.w3.org/TR/webauthn/#sec-authenticator-data) specification
- [CBOR](https://cbor.io) & [COSE](https://datatracker.ietf.org/doc/rfc8152/) encoding
- [ASN.1](https://www.w3.org/Protocols/HTTP-NG/asn1.html) encoding
- [big/little endian](https://en.wikipedia.org/wiki/Endianness) encoding
- [struct marshalling](https://learn.microsoft.com/en-us/dotnet/framework/interop/marshalling-classes-structures-and-unions)

... yikes 😅

I think probably the best way to break down this problem is to show the hierarchy of types within the attestation data and then detail their encoding and semantics.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gkh4jblv9f2wrdg8wu83.png)

- `attestation`: Represents the CBOR encoded map represented by the attestation data return from the App Attest service.
 - `fmt`: CBOR encoded string that identifies the statement format. For Apple's Device Check App Attest, it is always "apple-appattest".
 - `attStmt`: The attestation "statement", encoded as a CBOR map.
   - `x5c`: CBOR encoded array of byte arrays, each byte array is encoded as an X509 certificate.
     - Index `0`: The public certificate of the key pair generated by the app.
     - Index `1`: Intermediate signing certificate. The chain is represented by [`root`](https://www.apple.com/certificateauthority/private/) -> `intermediate` -> `auth cert`.
  - `receipt`: CBOR encoded byte array can be sent to the App App Attest service to get information on the risk metric associated with the attestation.
 - `authData`: Or ["Authenticator Data"](https://www.w3.org/TR/webauthn/#sctn-authenticator-data) is defined within the web authentication spec. For the most part, data in this structure is fixed width, so we can marshal the bytes directly to a struct (mostly 😅).
   - `rpidHash`: As the name suggests, this is a hash of the RPID, or [relying party identifier](https://www.w3.org/TR/webauthn/#relying-party-identifier). Within App Attest, the RP is always your app's ["App ID"](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/AppID.html).
   - `flags`: bitflags to describe the auth data; does it include attest data? Is the user verified? Is there extensions on this auth data? Stuff like that...
   - `signCount`: A counter for how many times the signature has signed content. When parsing the attestation statement, this figure is zero.
   - `attestedCredentialData`: Contains the [credential data](https://www.w3.org/TR/webauthn/#sctn-attested-credential-data) for the corresponding attestation. App Attest uses the key identifier as the credential identifier
     - `aaguid`: Determines if the app attest environment is development or production.
     - `credentialIdLength`: A big-endian `UInt16` value for the length `credentialId`.
     - `credentialId`: The attested credential identifier. For the purposes of app attest, the key identifier and the credential identifier are the same value.
     - `credentialPublicKey`: [COSE](https://datatracker.ietf.org/doc/rfc8152/) key, the encoding of which is a whole thing, but since the apple App Attest protocol does not make use of this field, I won't spend time going into it.

#### Decoding the Attestation Object

The first level of decoding the attestation object required CBOR decoding. To decode a CBOR encoded object you can pull in a library like [Dahomey.Cbor](https://github.com/dahomey-technologies/Dahomey.Cbor), or write your own. Somewhere in the middle is using what Microsoft provides with [`System.Formats.Cbor.CborReader`](https://learn.microsoft.com/en-us/dotnet/api/system.formats.cbor.cborreader?view=dotnet-plat-ext-7.0). You still need to do a lot of the lifting and write a deserialiser, but the class can help.

To understand CBOR encoding better, take a look at [my article here](https://dev.to/mnelsonwhite/deserialising-cbor-encoded-data-in-net-5cgo).