要点:Android は、デバイスに応じて複数ある署名形式のいずれかを検証します。v1(JAR 署名、ファイル単位、Android 6 以前向け)、v2(APK 署名ブロック内のファイル全体に対する署名、Android 7.0)、v3(v2 にローテーション証明の系譜を加えたもの、Android 9)、v3.1(Android 13 以降だけを対象とするローテーション)、v3.2(従来型と ML-DSA を組み合わせたハイブリッド署名、Android 17)、v4(増分インストール用の独立した .idsig ファイル、Android 11)です。API レベル 30 以上をターゲットとするアプリには、v2 以降の署名が必要です。アップデートは、同じ鍵か、アプリのローテーションの系譜で後継となる鍵で署名しなければなりません。そのため署名鍵は、アプリが存在する限りそのアプリのアイデンティティであり続けます。署名鍵は Google Play に保管してもらい(Play アプリ署名)、自分ではアップロード鍵だけを保持し、リリースは apksigner verify -v --print-certs で確認してください。
実験では、eclipse-temurin:25.0.4.1_1-jdk-noble 上の Java 25.0.4.1 で、Android SDK build-tools 37.0.0 の apksigner(自身のバージョン表示は 0.9)を使用しました。サンプルは、2026年10月22日に f-droid.org からダウンロードした F-Droid クライアントの APK です。それ以外の鍵はすべて、この記事のために生成したものです。
署名スキーム
| スキーム | 導入 | 格納場所 | 追加される内容 |
|---|---|---|---|
| v1 | Android 1.0 | META-INF/*.SF、*.RSA/*.EC | JAR 署名:ZIP エントリごとのハッシュ。ZIP メタデータは対象外 |
| v2 | Android 7.0(API 24) | APK 署名ブロック、ID 0x7109871a | ブロック自体を除くファイル全体に対する署名 |
| v3 | Android 9(API 28) | ID 0xf05368c0 | v2 の内容に加え、サポートする SDK の範囲とローテーション証明の系譜 |
| v3.1 | Android 13(API 33) | 新しいブロック ID | Android 13 以降でのみ適用されるローテーション |
| v3.2 | Android 17(API 37) | ID 0x70e1c89f | 従来型 1 つとポスト量子 1 つの、ちょうど 2 つの署名者。両方とも検証 |
| v4 | Android 11(API 30) | APK の隣の app.apk.idsig | adb install --incremental 用の、APK 全体に対するマークルツリー。v2 または v3 が必要 |
APK 署名スキーム v2 は「APK ファイルをひとかたまりのバイナリとして扱い、ファイル全体にわたって署名チェックを行う」ため、「ZIP メタデータの変更を含め、APK へのあらゆる変更が APK 署名を無効にする」ことになります(AOSP)。検証も高速になります。デバイスが検証のために各エントリを展開する必要がなくなったからです。v1 が必要なのは Android 7.0 より古いデバイスだけです。minSdkVersion が 24 以上の APK には不要で、apksigner も v1 署名を付けません。v1 を残しておいても、v2 署名の削除を隠すことはできません。v2 で署名され、v1 でも署名された APK は、.SF ファイルに X-Android-APK-Signed 属性を持ち、この属性は v1 署名の対象に含まれます。さらに検証者は、APK 自身が宣言しているスキームの署名が欠けている場合、その APK を拒否しなければなりません。
Android 11 以降は、「Android 11(API レベル 30)をターゲットとし、現在 APK 署名スキーム v1 のみで署名されているアプリは、APK 署名スキーム v2 以降でも署名する必要がある」とされています(動作の変更点)。
apksigner verify の出力の読み方
apksigner verify -v --print-certs は、APK に含まれるすべてのスキームを、APK がサポートする SDK の範囲について検証し、署名者を表示します。公開されている F-Droid クライアントの場合は次のとおりです(出力はスキームと署名者の行に絞っています)。
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v3.2 scheme (APK Signature Scheme v3.2): false
Verified using v4 scheme (APK Signature Scheme v4): false
Verified for SourceStamp: false
Number of signers: 1
V3.0 Signer: certificate DN: CN=Ciaran Gultnieks, OU=Unknown, O=Unknown, L=Wetherby, ST=Unknown, C=UK
V3.0 Signer: certificate SHA-256 digest: 43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab
V3.0 Signer: key algorithm: RSA
v1 が false なのはエラーではありません。このアプリの最小 SDK は 24 なので、v1 は不要です。証明書のダイジェストは、アプリの署名者を識別する値です。F-Droid は署名鍵のページで、クライアントの値としてまさにこの 43238d51…a9c9ccab を公開しており、Android はアップデートのたびに署名者を照合します。信頼の連鎖はありません。APK の証明書は通常は自己署名であり、v2 の検証ルールは、最初の証明書の SubjectPublicKeyInfo が署名鍵と一致することを確認するだけで、発行者や有効期間を確認するステップはありません(v2)。この証明書は 2010年に発行されたものです。
コピーを独自の P-256 鍵で再署名し、さらにファイルの 1 バイトだけを変更すると、このチェックの 2 つの側面を確認できます。
keytool -genkeypair -keystore key1.p12 -storetype PKCS12 -storepass changeit -alias app \
-dname "CN=Example App signing key 2026" -validity 10000 -keyalg EC -groupname secp256r1 -sigalg SHA256withECDSA
apksigner sign --ks key1.p12 --ks-pass pass:changeit --out app-key1.apk F-Droid.apk
apksigner verify -v --print-certs app-key1.apk
cp app-key1.apk tampered.apk
printf '\x00' | dd of=tampered.apk bs=1 seek=4000000 conv=notrunc status=none
apksigner verify -v tampered.apk
Verifies
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
V3.0 Signer: certificate DN: CN=Example App signing key 2026
V3.0 Signer: certificate SHA-256 digest: a93143a06f88894887d593f080c05b4ffd3aa9839c52378cee687987162d6d72
V3.0 Signer: key algorithm: EC
DOES NOT VERIFY
ERROR: APK Signature Scheme v3 signer #1: APK integrity check failed. CHUNKED_SHA256 digest mismatch. Expected: <66b1ea5c6952adc0c70f086346a76da06a5ab35dac770d104ff14d5ec09a9c97>, actual: <a5b1f3e628e868c150ade8cf1f3280f54ff4ae3771f20620cafbe97ad72ced57>
再署名したコピーは、証明書が異なるだけで問題なく検証に通ります。だからこそ、APK が正しい開発元のものかどうかを示すのは「Verifies」ではなく、署名者のダイジェストなのです。元の F-Droid に上書きインストールしようとすると、Android は別の鍵で署名されたアップデートとして拒否します。CHUNKED_SHA256 ダイジェストは、ファイルの各セクションを連続する 1 MB のチャンクに分けて計算されます。これにより、v2 以降では高速かつ並列に検証できます。
鍵のローテーション:v3 と v3.1
アプリは、単に再署名するだけでは署名鍵を変更できません。そのようなアップデートは拒否されます。v3 はローテーション証明(proof-of-rotation)を追加します。これは「各ノードに、以前のバージョンのアプリの署名に使われた署名証明書を含む単方向連結リスト」で、各証明書が次の証明書に署名します(v3)。古い鍵を知っているデバイスは、系譜によって両者がつながっていれば、新しい鍵で署名された APK を受け入れます。
apksigner rotate --out lineage.bin --old-signer --ks key1.p12 --ks-pass pass:changeit \
--new-signer --ks key2.p12 --ks-pass pass:changeit
apksigner sign --ks key1.p12 --ks-pass pass:changeit --next-signer --ks key2.p12 --ks-pass pass:changeit \
--lineage lineage.bin --out app-rotated.apk F-Droid.apk
apksigner verify -v --print-certs app-rotated.apk
apksigner lineage --in app-rotated.apk --print-certs
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): true
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) certificate DN: CN=Example App signing key 2027
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) key algorithm: EC
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32) certificate DN: CN=Example App signing key 2026
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32) key algorithm: EC
Signer #1 in lineage certificate DN: CN=Example App signing key 2026
Has installed data capability: true
Has shared UID capability : true
Has permission capability : true
Has rollback capability : false
Has auth capability : true
Signer #2 in lineage certificate DN: CN=Example App signing key 2027
Has installed data capability: true
Has shared UID capability : true
Has permission capability : true
Has rollback capability : false
Has auth capability : true
apksigner はデフォルトで、新しい鍵を Android 13 以降向けの v3.1 ブロックに入れ、Android 7〜12 向けの v3.0 ブロックには古い鍵を残します。これは意図的な動作です。AOSP は「Android 12(API レベル 31)以前では、APK の鍵のローテーションは推奨されません」としています。checkSignatures が系譜を認識して「最新の署名を返す」のは、Android 13 以降だけだからです。--rotation-min-sdk-version 28 を指定すると、Android 9 以降にもローテーションを強制的に適用できます。ただし apksigner のドキュメントでは、この指定は古いプラットフォームですでにローテーションを行ったアプリ向けとされています(apksigner)。
機能フラグ(capability)は、古い鍵に引き続き許可される操作を示します。その鍵で署名されたインストール済みデータの上にアップデートをインストールすること、UID を共有すること、署名権限を保持すること、そして(ここでは false)その鍵へロールバックすることです。古い鍵が危殆化した場合は、apksigner lineage でこれらを取り消してください。
ポスト量子:ハイブリッド方式の v3.2
Android 17 では v3.2 が追加されます。これは「Android エコシステムのポスト量子暗号への移行を支援するために設計されたハイブリッド署名スキーム」で、APK は「RSA や ECDSA などの従来型のアルゴリズムと、PQC アルゴリズムの両方で」署名されます。PQC アルゴリズムは当初は ML-DSA-65 または ML-DSA-87 で、署名は ID 0x70e1c89f のブロックに格納されます。このブロックには「ちょうど 2 つの署名者が含まれていなければならない」とされています(v3.2)。Android 17 は両方を検証します。それより古いバージョンはこのブロックをスキップして v3.0 または v3.1 を使うため、これらも引き続き含めておく必要があります。プラットフォームは PQC 鍵を従来型の鍵のローテーションとして扱い、2 つの署名者は「同一の過去の署名履歴」を持たなければなりません。
Java 25 の keytool は ML-DSA 鍵を生成でき(-keyalg ML-DSA-65)、build-tools 37.0.0 はその鍵で署名できます。ただし、うまくいくまでには試行錯誤が必要でした。ハイブリッドの署名者の両方に系譜を持たせるまで apksigner は NullPointerException で失敗し、さらに PQC 側の系譜は、従来型と同じ以前の鍵からのローテーションでなければなりませんでした。
keytool -genkeypair -keystore pq.p12 -storetype PKCS12 -storepass changeit -alias app \
-dname "CN=Example App PQ signing key" -validity 10000 -keyalg ML-DSA-65
apksigner rotate --out lineage-pq.bin --old-signer --ks key1.p12 --ks-pass pass:changeit \
--new-signer --ks pq.p12 --ks-pass pass:changeit
apksigner sign --ks key1.p12 --ks-pass pass:changeit --next-signer --ks key2.p12 --ks-pass pass:changeit --lineage lineage.bin \
--next-signer --ks key2.p12 --ks-pass pass:changeit --hybrid-signer-role classical --signer-lineage lineage.bin \
--next-signer --ks pq.p12 --ks-pass pass:changeit --hybrid-signer-role pqc --signer-lineage lineage-pq.bin \
--hybrid-min-sdk-version 37 --out app-hybrid.apk F-Droid.apk
apksigner verify -v --print-certs app-hybrid.apk
Verifies
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): true
Verified using v3.2 scheme (APK Signature Scheme v3.2): true
V3.2 Hybrid Classical Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) certificate DN: CN=Example App signing key 2027
V3.2 Hybrid Classical Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) key algorithm: EC
V3.2 Hybrid PQC Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) certificate DN: CN=Example App PQ signing key
V3.2 Hybrid PQC Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) key algorithm: ML-DSA
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) certificate DN: CN=Example App signing key 2027
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32) certificate DN: CN=Example App signing key 2026
これで 1 つの APK に 3 世代の鍵が含まれることになります。Android 7〜12 向けの key1、Android 13 以降向けの key2、そして v3.2 に対応したプラットフォーム向けの key2 と ML-DSA-65 鍵です。ハイブリッド署名の APK は、単一の署名者で署名したものより 20,480 バイト大きくなりました。--hybrid-min-sdk-version 37 を指定しても、この apksigner は v3.2 の範囲を「36 (dev release=true)」と表示しました。これは、開発中のプラットフォームに対して使われる表記です。リリースに使う build-tools のバージョンで出力を確認してください。ただし、ほとんどの開発者がこれらのコマンドを実行することはありません。ハイブリッド鍵は Play が生成・管理するからです(次のセクション)。
v4 と増分インストール
--v4-signing-enabled true を指定すると、APK の隣に .idsig ファイルが書き出されます。このファイルには、fs-verity 形式で APK 全体を対象としたマークルツリーと、そのルートに対する署名が格納されています。そのため adb install --incremental では、すべてのバイトが届く前にアプリを起動できます。AOSP は「v4 署名には、補完となる v2 または v3 署名が必要です」と明記しています。実験では、12.6 MB の APK に対して署名ファイルは 103,041 バイトで、apksigner verify --v4-signature-file app-v4.apk.idsig は Verified using v4 scheme (APK Signature Scheme v4): true と報告しました。v4 は開発とストリーミングのための機能であり、ストアにはアップロードされません。
Play アプリ署名とデベロッパーの確認
Google Play では、ユーザーがインストールするものに署名する鍵は、通常は開発者自身の鍵ではありません。Play アプリ署名では、開発者がアップロード鍵(「2048 ビット以上の RSA 鍵」)でアプリバンドルに署名し、Google は配信する APK を、Google 側で保管しているアプリ署名鍵で再署名します。これには 2 つの影響があります。
- アップロード鍵を紛失したり漏洩したりしても、回復できます。「アプリからロックアウトされることはありません」とされており、新しい証明書を用意して Play Console でアップロード鍵のリセットをリクエストします。
- アプリ署名鍵のローテーション(「鍵のアップグレード」)は、Play Console で行う操作です。Play は現在、新しいアプリを「Google 生成の鍵による量子対応のハイブリッド署名」に登録しています。鍵は「従来型の 4096 ビット RSA 鍵とポスト量子の ML-DSA-65 鍵」に古いデバイス向けの従来型の鍵を加えたもので、「結果としてアプリは 3 つの異なる鍵を使う」ことになります。Play によると、ハイブリッド署名は現時点では v4 と互換性がなく、Android 17 以降へのインストールについては年 1 回の鍵のアップグレードが提供されます。
Play 以外では、署名鍵の重要性はさらに高まります。Android デベロッパーの確認は、認定デバイスにインストールされるアプリについて、開発者とそのパッケージ名を署名鍵とともに登録する仕組みです。保護は 2026年9月30日にブラジル、インドネシア、シンガポール、タイで始まり、Google は 2027年にデベロッパーの確認を全世界に拡大する予定です。FAQ でははっきりと「署名鍵を紛失すると、パッケージを登録できなくなります」と書かれています。
よくある間違い
- 署名後に APK を変更する:
apksignerを使う場合、「zipalignは APK ファイルに署名する前に使用しなければならない」とされています(zipalign)。ZIP を書き換えるツールはすべて同様です。署名後に何かを変更すると、前述のダイジェストの不一致が発生します。 - 「Verifies」を信頼する:どの鍵でも有効な署名は作れます。署名者の証明書の SHA-256 ダイジェストを、想定している値と比較してください(自分のアプリの値は Play Console に表示され、F-Droid や多くのオープンソースプロジェクトは自分たちの値を公開しています)。
- 特に理由もなく Android 12 以前で v3 によるローテーションを行う:
apksignerに任せて v3.1 を対象にしてください。 - 署名鍵のコピーが 1 つしかない:Play 以外で配布するアプリにはリセットの手段がありません。鍵がアプリそのものです。
- 鍵をリポジトリに置く:アップロード鍵はハードウェアで保護された鍵ストアかクラウドの鍵ストアに保管し、CI には鍵ファイルではなく署名の権限を与えてください。
- 鍵のアップグレードを想定せずにバックエンドでアプリ署名証明書をピンニングする(API の許可リスト、OAuth クライアントの登録、証明書で制限した Firebase や Maps の API キー):Play のドキュメントでは、新しいフィンガープリントを API プロバイダーに登録するよう求めています。ハイブリッド署名では、新しい鍵が 2 つになります。