WWDC Quick Look 💓 By SwiftGGTeam
Get to know the ManagedApp Framework

Get to know the ManagedApp Framework

元の動画を見る

Highlight

ManagedApp フレームワークは iOS/iPadOS 18.4 および visionOS 2.4 で導入されました。MDM から管理対象 app に対して、設定値、パスワード、証明書、そしてハードウェアバインドされたキーをサポートする ACME ID を直接配布でき、変更時には async sequence を通じて app にリアルタイムで通知できます。


主要内容

エンタープライズ環境において、新しくインストールされた app の初回起動は、もっとも体験の悪い瞬間になりがちです。従業員はサーバーアドレス、ユーザー名、パスワード、二要素認証コードを入力し、さらに設定画面で組織のコンプライアンス要件に合わせて項目をひとつずつチェックしていく必要があります。Bob Whiteman はセッション冒頭でこの課題を率直に提示しました(00:51)。手順が増えればミスも増え、従業員は使うのをやめ、IT 部門にはチケットが積み上がり、最終的に組織はその app をリストから外すという結末を迎えます。開発者が自前で解決しようとすれば、ID フェデレーション、設定用 Web サイトの構築、認証局との連携、組織向けカスタムビルドの維持など、本来のコア機能とは無関係な作業を抱え込むことになります。

ManagedApp フレームワークの考え方は、こうした作業をすべてシステム側に委ねることです。管理者が MDM 側で設定を済ませておけば、app は起動した瞬間に必要な構成と鍵を受け取れるため、初回セットアップフローそのものが不要になります(02:25)。フレームワークは 4 種類のデータの安全な配布を一手に担います。app 独自の構成、パスワード、証明書、そして ID(PKCS #12、SCEP、ACME に対応し、ACME ではハードウェアキーへのバインドおよびリモート attestation も可能)です。この仕組みは MDM 側で declarative device management を利用していることを前提としており、app が管理対象になった瞬間から有効になり、すべての MDM 登録タイプをカバーします。

設計上もっとも重要なのは、構成 schema を Apple や MDM ベンダーが共通フォーマットとして定めるのではなく、app 開発者自身が定義するという点です。Bob はこの点を繰り返し強調しています(06:44)。自分の app をもっとも理解しているのは開発者本人であり、もっとも適切な構成可能項目を提示できるのも開発者だけだ、という考え方です。フレームワークは出来合いのソリューションではなく、ソリューションを構築するためのプラットフォームとして位置付けられています。


詳細

ManagedApp フレームワークは 4 つの独立した provider で構成されており、それぞれパスワード、証明書、ID、カスタム構成に対応します(12:20)。前者 3 つはシステム組み込みの型を返し、カスタム構成のデータ構造のみ app 側で自由に決められます。

最初のステップは、カスタム構成の schema を Decodable な型として記述することです。Landmarks のサンプルでは collection というフィールドがひとつあるだけのシンプルな構造になっています。

// Your app's managed configuration

struct LandmarksManagedConfig: Decodable {

    private(set) var collection: LandmarkCollection?

    private enum CodingKeys: String, CodingKey {
        case collection
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        collection = try values.decode(LandmarkCollection.self, forKey: .collection)
    }
}

ポイントは次のとおりです。

  • LandmarksManagedConfigDecodable に準拠しており、フレームワークはこの型を使って MDM から配布された辞書を Swift の型へデシリアライズします。
  • collectionprivate(set) で読み取り専用ビューとして公開され、構成はフレームワーク経由でのみ更新されます。
  • CodingKeys でキー名を明示しておき、プロパティ名による暗黙的なマッチングに依存しないようにしています。
  • init(from:) では decodeIfPresent ではなく decode を使っています。フィールドが欠けていればエラーになるため、schema が一致しないケースで app を早めに失敗させられます。

次のステップは、構成を app のランタイムに注入することです。ManagedAppConfigurationProviderAsyncSequence を返し、構成が変更されるたびにループ本体が再度実行されます(0:02)。

// Receiving the current configuration

import ManagedApp

// [...]

var managedCollection: LandmarkCollection?

// [...]

func loadCollections() {
    // [...]
    Task {
        let configProvider = ManagedAppConfigurationProvider()

        for await config in await configProvider.configurations(LandmarksManagedConfig.self) {
            // config's type is LandmarksManagedConfig?
            managedCollection = config?.collection
        } // Loops forever
    }
}

ポイントは次のとおりです。

  • import ManagedApp でフレームワークを取り込みます。モジュール名はそのまま ManagedApp です。
  • configurations(_:) は schema 型を引数に取り、要素の型は LandmarksManagedConfig? になります。構成が無い場合や、app が非管理状態の場合は nil が流れてきます。
  • for await ループは終了せず、MDM から新しい構成がプッシュされるたびにイテレーションが回ります。手書きの KVO や NotificationCenter の購読が不要になります。
  • Task をデータロード処理の中に置くことで、構成の反映とデータロードを同じライフサイクルで扱えます。

3 つめのステップは ID 認証の処理です。VPN 拡張やエンタープライズサービスへの呼び出しではクライアント証明書がよく必要になりますが、フレームワークは SecIdentity をそのまま URLSession の delegate に渡せます。

// Using an identity

final class MyURLSessionDelegate: NSObject, URLSessionDelegate {

    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge)
        async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        switch challenge.protectionSpace.authenticationMethod {
        case NSURLAuthenticationMethodClientCertificate:

            // Look up the identity
            let provider = ManagedAppIdentitiesProvider()
            let id = "AssetDownloadClient"
            guard let identity = try? await provider.identity(withIdentifier: id) else {
                // No identity, cancel the challenge
                return (.cancelAuthenticationChallenge, nil)
            }

            // Use the identity to authenticate.
            return (.useCredential, URLCredential(identity: identity,
                                                  certificates: nil,
                                                  persistence: .forSession))
        default:
            return (.performDefaultHandling, nil)
        }
    }
}

ポイントは次のとおりです。

  • URLSessionDelegate の async 版は challenge が到着すると一旦サスペンドし、ID を取得してからコールバックします。
  • ManagedAppIdentitiesProvider().identity(withIdentifier:) は schema で取り決めた識別子を使って ID を取得します。識別子は app 側で自由に定義できます。
  • ID が取得できない場合は cancelAuthenticationChallenge を返し、リクエストを弱い認証へフォールバックさせず即座に失敗させます。
  • URLCredentialpersistence には .forSession を指定することで、秘密鍵が Keychain の外へコピーされることを防ぎます。

ライセンス管理はこのフレームワークがもたらす改善がもっとも分かりやすく現れるユースケースです。Bob は 09:00 で従来のアプローチと比較しています。文字列トークンを配布する方式は漏洩リスクと隣り合わせですが、ManagedApp フレームワークでは組織の CA が発行する暗号 ID を配布でき、秘密鍵はデバイス上で生成され、デバイスから外に出ることはありません。VPN 拡張ではさらに踏み込んで、ハードウェア attestation 付きの ACME ID を取得できるため(10:46)、VPN 認証情報を狙った中間者攻撃の経路を根本から塞げます。


重要ポイント

  • 既存の ManagedAppConfiguration を ManagedApp フレームワークへ移行する: 取り組む価値の理由は、旧方式が app 拡張に対応していない、証明書や ID を扱えない、変更通知の仕組みも無いという制約を抱えているためです。新フレームワークは iOS/iPadOS 18.4 以降で、より完全な後継となります(09:46)。始め方としては、まず最小限の schema を定義して「MDM のプッシュ → app が受信する」という経路を疎通させ、その後で旧来の UserDefaults からの読み出しロジックを項目単位で移し替え、移し替えるたびに古いコードを削除していくのが扱いやすい進め方です。

  • VPN 拡張にハードウェアバインドされた ACME ID を組み込む: 取り組む価値の理由は、VPN が攻撃対象としてもっとも狙われやすい入口のひとつであり、秘密鍵を Secure Enclave に固定し、リモート attestation を併用することで、組織の VPN アクセス制御ポリシーに本当の意味で信頼性を持たせられるからです(10:30)。始め方としては、schema に ID フィールドを宣言し、MDM 側で ACME 経由の発行を要求し、NEVPNManager の認証コールバック内で先ほどの ManagedAppIdentitiesProvider のコードを再利用するとスムーズです。

  • ライセンストークンをライセンス ID へ置き換える: 取り組む価値の理由は、トークンは一度漏洩すると組織側でコストを払い続けるしかなくなる一方、デバイス側で生成された鍵ペアはデバイスから出ることがなく、漏洩面がほぼゼロに抑えられるためです(08:30)。始め方としては、まずサーバー側で mTLS をサポートし、証明書発行フローを組織の CA に接続したうえで、クライアント側で ManagedAppIdentitiesProvider から取得した ID をクライアント証明書認証に利用するとよいでしょう。

  • SSO 拡張でワンタイムパスワードや初期クレデンシャルを受け取る: 取り組む価値の理由は、ID プロバイダ側が独自の認証プロトコルを設計していることが多く、これまでユーザーに手動でクレデンシャルを入力させるしかなかったところを、管理者が MDM 経由で初期パスワードやバインディングトークンを直接プロビジョニングできるようになるからです(11:42)。始め方としては、SSO 拡張に ManagedAppPasswordsProvider を追加し、起動時に読み出しを試み、値が存在すれば手動入力フローをスキップする実装にするとよいでしょう。

  • 構成変更を async sequence で受け取り全体をリロードする: 取り組む価値の理由は、for await ループは複数回の更新を素直に扱えるため、フィールドごとに監視することによる中間状態の不整合を避けられるからです(06:00)。始め方としては、Task 内で configurations(_:) をイテレートし、新しい値を受け取るたびに「構成を反映する」一連の処理を最初から走らせる構成にして、フィールド単位の差分適用を避ける形にするとシンプルです。


関連セッション

コメント

GitHub Issues · utterances