Android 17でCertificate Transparencyが既定ON — 自前CAのHTTPS傍受が壊れる理由
はじめに
Android 17(API level 37)を targetSdk に指定したアプリでは、Certificate Transparency(CT)の検証が既定で有効になる。Android 16 では opt-in、Android 15 以下では機能自体が存在しなかったので、既定 ON はこのバージョンが初めてだ。
影響が大きいのは、mitmproxy や Charles、HTTP Toolkit で HTTPS 通信の中身を覗いている側だ。自前で生成した CA の証明書は公開 CT ログに載っていないので、CT を要求された時点で検証に落ちる。Android には CT を切る設定がちゃんと用意されているのに、傍受したい相手が自分のアプリでない限りその設定には手が届かない。この記事では、既定 ON で何が起きるのか、公式の逃げ道がどこまで使えるのか、そして CT が勝手に無効化される 70 日ルールまでを見ていく。
何が変わったか
Android 17 は 2026年6月16日にリリースされた。挙動変更のドキュメントに書かれているのは一行で、「API level 37 以上を target にするアプリでは CT が既定で有効になる」というものだ。バージョンごとの扱いを並べるとこうなる。
| OS / target | CT の既定 |
|---|---|
| Android 15(API 35)以下 | 機能なし |
| Android 16(API 36) | 無効(opt-in 可) |
| Android 17(API 37)以上 | 有効(opt-out 可) |
これが効くのは「Android 17 端末で動いている、API 37 を target にしたアプリ」の組み合わせだ。つまり今日時点ではまだ取りこぼしが多い。ただし Google Play は 2027年8月から新規アプリとアップデートに API 37 を要求する見込みで、そこを過ぎれば更新され続けているアプリは全部この条件に入る。
検証に落ちたときに出るのは SSLPeerUnverifiedException や、CT 固有のメッセージを含む例外だ。HTTP Toolkit の記事では Certificate chain does not conform to required transparency policy: NOT_ENOUGH_SCTS という文言と、Chrome 側の NET::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED が挙げられている。証明書の期限切れやホスト名不一致とは別のエラーなので、メッセージを読めば切り分け自体は付く。
なぜ自前CAでは満たせないのか
CT は「発行された証明書を公開ログに記録し、記録されたことの署名付きレシートを証明書に添える」仕組みだ。このレシートが SCT(Signed Certificate Timestamp)で、Android のポリシーは必要な SCT の本数を証明書の有効期間で切り分けている。
埋め込み SCT の場合、有効期間 180日以下なら異なる CT ログから 2つ、180日を超えるなら3つ。加えて、そのうち少なくとも 2つは Android が認識している異なるログ運用者から発行されたものでなければならない。OCSP や TLS 拡張で渡す場合は、チェック時点で Qualified / Usable / ReadOnly のいずれかだったログから 2つ以上、かつ運用者が 2つ以上異なること、という条件になる。
「Android が認識しているログ運用者」の一覧は https://www.gstatic.com/android/certificate_transparency/v3/log_list.json に日次で公開され、端末は1日1回これを取りに行く。ここが決定的で、公開 CT ログは任意の第三者が持ち込んだ CA の証明書を受け付けない。ローカルで openssl を叩いて作った CA が発行する証明書は、原理的に SCT を取得できない。証明書の作り方を工夫すれば通る、という類の話ではない。
もうひとつ押さえておきたいのが CA ストアの構造だ。Android の CA は、Conscrypt APEX モジュールで管理されるシステムストアと、ユーザーが手で入れるユーザーストアに分かれている。API 24 以降のアプリは既定でユーザーストアを信頼しないため、他人のアプリの通信を見るには root 化してシステムストアに CA を差し込むのが定番手順になっていた。CT が効くのはまさにこのシステムストアに連なる証明書チェーンで、これまで有効だった唯一の経路が塞がれた形になる。
逃げ道はあるが、他人のアプリには届かない
公式の opt-out は network security config の <certificateTransparency> 要素だ。<base-config> に置けば全体、<domain-config> に置けばドメイン単位で切れる。
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">secure.example.com</domain>
<certificateTransparency enabled="false"/>
</domain-config>
</network-security-config>
さらにドキュメントには、CT 検証はカスタムトラストアンカーを使う接続では実行されない、と書かれている。<trust-anchors> に src="user" や @raw/my_ca のような自前証明書を指定した <domain-config> では CT が自動的に無効になり、そこに enabled="true" を明示しても上書きはできない。
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<trust-anchors>
<certificates src="@raw/my_ca"/>
</trust-anchors>
<!-- CT は自動で無効。enabled="true" でも戻らない -->
</domain-config>
問題は、この2つの逃げ道がどちらもアプリ側の設定だという点だ。自分がビルドしているアプリなら、デバッグビルドの debug-overrides に傍受用 CA を入れるなり対象ドメインだけ CT を切るなりで解決する。だが他人のアプリの通信を見たいとき、その APK の network_security_config.xml は書き換えられない。システムストアに CA を入れる手法は他人のアプリにも効くが CT に落ちる。CT を回避できる設定はアプリを所有していないと書けない。効く対象と使える手段が噛み合っていないのが、今回の変更の本質的な厄介さだ。
HTTP Toolkit の著者はこれに対して、CA 証明書から CT ログ運用者の鍵ペアを2つ導出し、証明書発行時に両方の SCT を埋め込んでから CA で署名し、端末側の CT ログ設定も CA 注入と同じマウント機構で差し替える、というアプローチを取っている。CA 証明書1つから下流が全部導出できるので設定の受け渡しが要らない、という設計だ。ただしマウントは再起動で消えるし、端末側の CT ログ設定には少なくとも3種類のフォーマットがあると書かれている。root 化前提の、それなりに重い手当てになる。
エンジニアへの影響
自分のアプリを持っている側で先に確認すべきなのは、デバッグ手順ではなく本番の通信先だ。社内 PKI やプライベート CA で発行した証明書のエンドポイントを叩いているなら、targetSdk を 37 に上げた瞬間にそのドメインが落ちる。Play の期限(2027年8月)に押されて上げることになるので、上げる前に該当ドメインを <certificateTransparency enabled="false"/> で opt-out しておく。ここで <base-config> ごと切ってしまうと公開サイトへの接続まで CT が効かなくなるので、ドメイン単位で絞るほうがいい。
デバッグ環境側は、Android 17 実機・エミュレータでの HTTPS 傍受手順が今後通らなくなる前提で組み直すことになる。自社アプリなら debug-overrides に傍受用 CA を書くのが正攻法で、リリースビルドには影響しない。逆に言えば、デバッグビルドを用意していないアプリや、サードパーティアプリの挙動調査を手順に組み込んでいる場合は、代替を考える必要がある。
運用側で知っておく価値があるのが 70 日ルールだ。ポリシーには、端末上にログリストが無い場合、またはログリストのタイムスタンプが 70日より古い場合、CT の強制が無効になると書かれている。ネットワークから切り離した検証端末では、70日放置すれば CT が勝手に止まる。デバッグの逃げ道として使える一方で、「本番端末でも条件次第で CT が無効になり得る」という意味でもある。CT をセキュリティ対策として数える場合は、この抜け道を前提に置いたほうがいい。
まとめ
- Android 17(API 37)を target にしたアプリでは CT が既定で有効になり、システム CA に連なる証明書は SCT を要求される
- 自前 CA は公開 CT ログに載せられないため SCT を取得できず、原理的に条件を満たせない
- opt-out は
<certificateTransparency enabled="false"/>、カスタムトラストアンカー指定でも CT は自動で無効になるが、どちらもアプリ側の設定なので他人のアプリには使えない - 必要な SCT は有効期間180日以下で2つ、超えると3つ。うち2つは異なるログ運用者から
- ログリストが無いか 70日より古い場合は CT の強制自体が無効になる
Play の API 37 要求が 2027年8月に来るので、影響が本格的に出るのはそこからになる。自社アプリのプライベート CA 経路の洗い出しは、targetSdk を上げる作業に先立って済ませておきたい。
今日のその他のニュース
OpenAI が GPT-6 の2モデル「Sol」「Luna」を発表 — Hacker News で 1700pt を集め、本日最大の話題になった。同日には GPT-6 Astra が2005年以降未解読だった Enigma 暗号文を解いたという報告(715pt)も上がっている。(HN)
Claude Code の AGENTS.md がテレメトリ有効時しか読まれないバグ、根本原因が判明 — 「置いたのに読まれない」挙動の正体はテレメトリ有効時のみ読み込む実装で、既に修正済み。書き方を疑っていた人は実装側の問題だった可能性が高い。(HN 399pt)
Ubuntu の coreutils Rust 置換が完了段階に — ls などの基本コマンドが GNU 実装から Rust 実装へ切り替わる作業が仕上げに入った。スクリプトが GNU 固有のオプションに依存している場合は影響を受ける。(記事)
ソース
- Android 17 enables certificate transparency, and breaks custom CAs — HTTP Toolkit
- Network security configuration — Android Developers
- Android Certificate Transparency Policy — Android Developers
- Behavior changes: Apps targeting Android 17 or higher — Android Developers
- Meet Google Play’s target API level requirement — Android Developers