ねこ島

iOSのこととか、日常について雑に書きます

iOS14から追加された「App Attest API」でアプリの不正使用を防ぐ

引き続きiOS14ネタとなります。

今回は、DeviceCheckフレームワークに新たに追加された「App Attest API」についてみていきます。

本記事は公開済みのドキュメントを元に作成しています。今後仕様変更などにより記載内容と異なる場合があります。予めご了承ください。

App Attest APIとはなにか

App Attest APIは雑にいうとアプリの不正使用をサーバー側で防ぐAPIです。

Appleバイスが生成した証明書とサービスアカウントの紐づけを行い、サービス利用時にそのAppleバイスだけが持つ秘密鍵で署名したデータをサーバー側で検証することで、サーバーへアクセスするデバイスを確実にできます。

また検証の際に署名やApp IDの確認が行われるため、サイドローディングされたアプリや改造アプリに対しても効果を発揮します。

サイドローディング... ストア(App Store)を経由せずにアプリを入手、インストールすること。iOSではデベロッパーアカウントを使って再署名することで可能となる。Androidでいう野良アプリのインストール。

さらにAppleが公開しているREST APIを呼ぶことで、不正行為を行っている可能性を評価したパラメータが取得できます(この評価はデバイス上の一意の証明書のおおよその数を元にしているようです)。

ただしJailbreakによってiOS自体に改変が加えられている場合、確実に不正使用を防げるわけではないため、不正対策の一環として考えておいた方が良いです。

どういったときに使えるのか

以下のようなサーバー側で制御ができるコンテンツに対して効果的です。

  • 課金した人が使えるコンテンツへのアクセス
  • オンラインゲーム

また実際に不正が確認された場合、DeivceCheckのAPIを使うことでより強力なハードウェアBANを行うことができます。

詳しくはこちら

qiita.com

どのような仕組みか

f:id:nekowen:20200810204436p:plain

ほぼWebAuthnの仕組みに沿っており(といってもWebAuthnを知ったのがつい最近なので違っていたら教えて欲しいです‥🙇‍♂️)

流れとしてはこんな形です。

  1. 初回起動またはアカウント作成時にキーペアを生成し、サーバーサイドに送ります。
  2. サーバー側は送られてきたデータから検証を行い、問題なければ公開鍵とレシートをユーザー情報と紐付けます。
  3. クライアントは認可が必要なAPIにアクセスするタイミングで署名を行ったデータを一緒に送信します。
  4. サーバー側は送られてきたデータから検証を行い、問題なければ通信成功とします

実装方法(クライアント)

1. サポートされているかどうかの確認

まずはAPIが使えるデバイスかどうかを確認する必要があります。

サポートされているかどうかはDCAppAttestServiceのisSupportedを確認します

let service = DCAppAttestService.shared
if service.isSupported {
    // サポートされている
}

2. キーペアの生成と検証を行う(インストール・アカウント作成時のみ)

クライアント側でgenerateKey(completionHandler:)を呼びキーペアを生成します。 キーペアは生成と同時にSecure Enclaveに保存されており、アプリ側から読み取ったり変更はできません。

keyIdが引数に渡されますが、これは生成されたキーペアに紐づいており、今後署名するときに必ず使うのでキーチェーン、あるいはファイルに保存します。

service.generateKey { keyId, error in
    guard error == nil else {
        //  Error
        return
    }
    
    //  Save KeyId
}

次に、サーバーからChallengeを受け取り、クライアント側でSHA256のハッシュ値を生成します。

let hash = Data(SHA256.hash(data: fromServerData))

keyId・ハッシュ値が揃ったら、attestKey(_:clientDataHash:completionHandler:)を呼びキーペアが有効であることをAppleに検証してもらいます。

成功するとDate型のAttestation Objectが渡ってくるのでこれをサーバーに送ります。

service.attestKey(keyId, clientDataHash: hash) { attestation, error in
    guard error == nil else {
        return
    }
    
    let attestationBase64 = attestation.base64EncodedString()
    // Base64に変換するかバイナリとしてそのままサーバーへ送る
}

※ちなみに、iOS14 beta 3まではこのメソッドを呼ぶと必ずエラーが帰ってきていました。beta 4にアップデートしたところ改善したので古いバージョンをお使いの場合は注意してください

サーバー側は送られたAttestation Objectを検証し、問題がなければ公開鍵とレシートをアカウントに紐づけてキーペアの生成は完了です。

3. アプリの有効性を検証する

全ての通信、あるいはビジネス上不正が起こっては困る通信に対してクライアント側で署名を行ったオブジェクトを送信します。

署名にはgenerateAssertion(_:clientDataHash:completionHandler:)を呼びます。

service.generateAssertion(keyId, clientDataHash: hash) { assertion, error in
    guard error == nil else {
        return
    }
    
    let assertionBase64 = assertion.base64EncodedString()
    // Base64に変換するかバイナリとしてそのままサーバーへ送る
}

ここで得られたAssertion Objectをサーバーに送ります。 サーバー側は送られたAssertion Objectを検証し、問題がなければ処理を進められます。

サーバー側の実装・仕様はAppleのドキュメントがあるのでこちらをどうぞ。

https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server

参考

https://developer.apple.com/documentation/devicecheck

https://developer.apple.com/documentation/devicecheck/establishing_your_app_s_integrity

engineering.mercari.com