RORK LABEN
PLAY — Google Play の対象 API レベル要件の期限が8月31日でした。ビルダーが生成するプロジェクトが追随しているかは、任せきりにせず確かめておきたいところですPOLICY — Play が低価値アプリの線引きを明確にしました。既存アプリの焼き直しや、モデルを包むだけで固有の有用性がないものが該当するとされていますDATA — サードパーティの AI 連携にもユーザーデータ要件が適用されます。外部モデルへ何を送っているかの開示責任は、アプリを公開する開発者の側にありますAPPLE — ガイドライン 2.5.2 の運用が厳格になり、実行時のコード実行環境を持つアプリへの審査が厳しくなりました。ツールとアプリの境界が問われていますBUILD — 一方で、サーバー側で署名済みのネイティブバイナリを再現可能なパイプラインで生成する方式は 2.5.2 の対象外です。技術的な違いは書き分けが要りますIOS — iOS 27 が9月に一般公開されます。対応は iPhone 11 以降です。新しい OS が来たときの実機確認は、毎年の恒例作業として組み込んでおくと安心ですPLAY — Google Play の対象 API レベル要件の期限が8月31日でした。ビルダーが生成するプロジェクトが追随しているかは、任せきりにせず確かめておきたいところですPOLICY — Play が低価値アプリの線引きを明確にしました。既存アプリの焼き直しや、モデルを包むだけで固有の有用性がないものが該当するとされていますDATA — サードパーティの AI 連携にもユーザーデータ要件が適用されます。外部モデルへ何を送っているかの開示責任は、アプリを公開する開発者の側にありますAPPLE — ガイドライン 2.5.2 の運用が厳格になり、実行時のコード実行環境を持つアプリへの審査が厳しくなりました。ツールとアプリの境界が問われていますBUILD — 一方で、サーバー側で署名済みのネイティブバイナリを再現可能なパイプラインで生成する方式は 2.5.2 の対象外です。技術的な違いは書き分けが要りますIOS — iOS 27 が9月に一般公開されます。対応は iPhone 11 以降です。新しい OS が来たときの実機確認は、毎年の恒例作業として組み込んでおくと安心です
記事一覧/開発ツール
開発ツール/2026-09-03初級

リンクを押してもアプリが開かないとき、疑うのは assetlinks.json の置き場所より先に署名鍵です

Android App Links が検証されずブラウザに吸われる原因の多くは指紋の食い違いです。keytool の実出力と、手元で照合できる小さな検査スクリプトの実行結果を添えて整理しました。

Android App Linksassetlinks.jsonExpo197Rork550個人開発207

テスト端末を並べて確かめていた夕方のことです。アプリは入っているのに、メールに貼った自分のサイトのリンクを押すと、ブラウザが開きました。アプリ内から Linking.openURL を呼べば目的の画面まできちんと着きます。外から入ってきたときだけ、アプリを素通りしてしまうのです。

個人開発でアプリをいくつも抱えていると、こういう「片側だけ動く」不具合にいちばん時間を取られます。動いている経路があるぶん、壊れている側を探す取っ掛かりが少ないのです。

先に結論をお伝えします。私の場合、assetlinks.json の置き場所も JSON の構造も合っていて、間違っていたのはその中に書いた指紋のほうでした。手元でビルドした版の署名鍵から取った指紋を貼っていて、ストアから配信されている版の署名とは別物だったのです。

Android 12 から、検証されていないリンクは既定で開きません

Android App Links は「この URL を開く権利をこのアプリに委ねます」という宣言を、サイト側とアプリ側の両方に置いて突き合わせる仕組みです。サイト側に置くのが https://あなたのドメイン/.well-known/assetlinks.json、アプリ側に置くのが autoVerify を付けた intent-filter になります。

ここで押さえておきたいのが、Android 12(API レベル 31)以降の既定の挙動です。検証が通っていないリンクは、ユーザーが設定画面で手動により有効化しないかぎり、アプリでは開きません。ブラウザへ流れます。

つまり検証に失敗しても、エラーは何も出ません。クラッシュもせず、ログにも赤い行は残らず、ただ「リンクを押したらブラウザが開いた」という結果だけが手元に残ります。私自身、最初はアプリ側のルーティングを疑って半日ほど無駄にしました。外から入ってくる導線が壊れたときは、アプリの中ではなく、アプリとサイトのあいだを先に見ます。

手順は3つで、詰まるのはたいてい3つめです

Rork や Expo で作ったプロジェクトなら、アプリ側の宣言は app.json(あるいは app.config.ts)に書きます。

{
  "expo": {
    "android": {
      "package": "net.example.myapp",
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            { "scheme": "https", "host": "example.com", "pathPrefix": "/app" }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

autoVerifytrue にすると、アプリのインストール時に端末が https://example.com/.well-known/assetlinks.json を取りに行き、そこに自分の署名の指紋があるかどうかを確かめます。

サイト側に置くファイルは次の形です。

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "net.example.myapp",
      "sha256_cert_fingerprints": ["ここに指紋"]
    }
  }
]

配信の条件は素っ気ないほど単純で、HTTPS であること、リダイレクトを挟まないこと、application/json で返すことの3点です。Lab のサイトは Cloudflare、読み物のブログは WordPress で運用しておりますが、静的ファイルを素直に置ける前者に対して、後者ではパーマリンクの設定が .well-known へのアクセスを飲み込んでしまうことがあります。置いたつもりのファイルが 404 を返していないか、まずブラウザで直接開いて確かめておくと落ち着きます。

そして3つめが指紋です。ここが本題になります。

keytool が出す形を、そのまま貼ります

指紋は署名鍵の証明書から取ります。手元の keystore なら keytool で読めます。実際にサンプルの keystore を作って実行した出力が、こちらです。

$ keytool -list -v -keystore upload.jks -storepass changeit
Alias name: upload
Certificate fingerprints:
	 SHA256: 6E:66:8F:80:8A:86:71:93:42:09:66:DB:3B:7F:AB:AE:4B:52:35:EC:E2:C8:76:EA:D9:F8:C5:9B:E8:E6:51:F7
Signature algorithm name: SHA256withRSA

SHA256: の右にある文字列を、コロンごと、大文字のまま sha256_cert_fingerprints に入れます。手元で数えたところ、コロンを含めて 95 文字、16 進の桁だけなら 64 桁でした。整形しようとしてコロンを外したり、小文字に直したりする必要はありません。むしろ触らないほうが安全です。

openssl しか手元にないときは、同じ値を次の形で取り出せます。

$ openssl x509 -in cert.pem -noout -fingerprint -sha256
sha256 Fingerprint=AF:B8:01:5B:D8:47:87:2A:59:A5:3F:F1:ED:61:2C:E3:14:D8:EE:AF:1F:BA:68:ED:A8:9A:31:FF:3C:67:7B:8D

手元の版と、配信されている版は別の鍵で署名されています

ここが私のつまずいた場所です。Google Play のアプリ署名を使っている場合、開発者が持っているのはアップロード鍵で、ユーザーの端末に届く APK に実際に載る署名は、Play 側が保管しているアプリ署名鍵になります。この2つは別の証明書ですから、指紋も当然に違います。

サンプルの keystore を2つ作って、指紋を並べてみました。

upload      = 6E:66:8F:80:8A:86:71:93:42:09:66:DB:3B:7F:AB:AE:4B:52:35:EC:E2:C8:76:EA:D9:F8:C5:9B:E8:E6:51:F7
appsigning  = AE:4E:BC:69:E7:8E:51:0F:DC:BA:C2:38:CC:7A:2E:37:0A:E7:57:2C:5C:11:9E:67:35:AB:04:43:E7:11:07:4B
一致するか  = no

手元のビルドではリンクが開くのに、ストアから入れ直すと開かない——という現象は、この食い違いで説明がつきます。エラーが出ないぶん疑う順番を間違えていたのだと気づいたのは、配信版を入れ直したあとでした。手元の版はアップロード鍵で署名されていますから、アップロード鍵の指紋しか書いていない assetlinks.json とは一致します。配信版は別の鍵ですから、一致しません。

対処はごく簡単で、両方の指紋を並べて書くだけです。sha256_cert_fingerprints は配列ですから、2つ以上入れられます。アプリ署名鍵の指紋は Play Console の「アプリの完全性」(アプリ署名)のページに表示されていますので、そこからコピーします。

どこで使われるか指紋の入手先
アップロード鍵手元のビルド、Play への提出手元の keystore を keytool -list -v
アプリ署名鍵ユーザーの端末に届く版Play Console のアプリ署名ページ

署名鍵まわりで提出そのものが止まる場合は、Rorkアプリを Google Play にアップロードできない署名キーエラーの直し方のほうが近い話題になります。

照合を目視でやめて、小さな検査に任せます

64 桁の 16 進を目で比べるのは、私には無理でした。ファイルを読んで機械的に突き合わせるだけの短いスクリプトを置いてあります。

import json, re, sys
 
REL = "delegate_permission/common.handle_all_urls"
FP_RE = re.compile(r"^(?:[0-9A-F]{2}:){31}[0-9A-F]{2}$")
 
def check(path, package, expected):
    problems = []
    try:
        doc = json.loads(open(path, encoding="utf-8").read())
    except json.JSONDecodeError as e:
        return [f"JSON として読めません: {e}"]
 
    if not isinstance(doc, list):
        problems.append("最上位が配列ではありません(1件でも [] で囲みます)")
        doc = [doc]
 
    hit = False
    for i, st in enumerate(doc):
        tgt = st.get("target", {})
        if tgt.get("namespace") != "android_app":
            continue
        if tgt.get("package_name") != package:
            problems.append(f"[{i}] package_name が {tgt.get('package_name')!r}")
            continue
        hit = True
        if REL not in st.get("relation", []):
            problems.append(f"[{i}] relation に {REL} がありません")
        fps = tgt.get("sha256_cert_fingerprints", [])
        for fp in fps:
            if not FP_RE.match(fp):
                problems.append(f"[{i}] 指紋の書式が違います: {fp[:20]}...")
        # 比較のときだけコロンと大小文字を無視します
        norm = {fp.replace(":", "").upper() for fp in fps}
        if expected.replace(":", "").upper() not in norm:
            problems.append(f"[{i}] 照合したい署名鍵の指紋が含まれていません")
 
    if not hit:
        problems.append(f"{package} を対象にした android_app の文が1件もありません")
    return problems
 
if __name__ == "__main__":
    path, package, expected = sys.argv[1], sys.argv[2], sys.argv[3]
    found = check(path, package, expected)
    print(f"--- {path}")
    print("  問題は見つかりませんでした" if not found else "")
    for p in found:
        print(f"  NG: {p}")

崩し方を変えた4つのファイルに通した実際の出力が、次のものです。

--- samples/ok.json
  問題は見つかりませんでした
--- samples/obj.json
  NG: 最上位が配列ではありません(1件でも [] で囲みます)
--- samples/nocolon.json
  NG: [0] 指紋の書式が違います: 6E668F808A8671934209...
--- samples/badrel.json
  NG: [0] relation に delegate_permission/common.handle_all_urls がありません

そして本題の食い違いを再現したのが、こちらになります。アップロード鍵の指紋だけを書いたファイルを、配信版の署名(アプリ署名鍵)で照合しています。

--- samples/uploadonly.json
  NG: [0] 照合したい署名鍵の指紋が含まれていません
--- samples/both.json
  問題は見つかりませんでした

両方を並べて書いた both.json は、アプリ署名鍵で照合しても、アップロード鍵で照合しても通りました。デプロイ前にこの2回を回しておけば、少なくとも指紋を理由に無言でブラウザへ流れることはなくなります。

書式の検査では大文字とコロンを厳しく見て、指紋の一致を見るところではコロンと大小文字を無視している点だけ、意図をお伝えしておきます。書き込むときは公式の例に揃えておきたい一方で、比較のときに書式の差で見落とすのは本末転倒だと考えたためです。

端末側の判定を、最後に見に行きます

ファイルが正しくても、端末が検証を終えていなければリンクは開きません。実機で状態を確かめられます。

# 現在の検証状態を確認します
adb shell pm get-app-links net.example.myapp
 
# 検証をやり直させます
adb shell pm verify-app-links --re-verify net.example.myapp

出力の各ドメインに付く状態が verified になっていれば通っています。nonelegacy_failure のままなら、assetlinks.json の配信か指紋のどちらかがまだ噛み合っていません。端末はインストール時に一度取りに行くだけですから、ファイルを直したあとはアプリを入れ直すか、上の --re-verify を明示的に走らせる必要があります。

インストール直後の着地先の設計まで含めて考えたい場合は、インストール直後に「目的の画面」へ着地させる — Rork アプリの遅延ディープリンク設計に別の角度でまとめてあります。

次に手を動かすなら

assetlinks.json を開いて、sha256_cert_fingerprints の中身が1つだけになっていないかを見てください。1つしかないなら、それが手元の鍵なのか Play が持つ鍵なのかを確かめるところが出発点になります。多くの場合、そこにもう1行足すだけで済みます。

私も配信版で試すまで気づけませんでした。同じところで止まっている方の、遠回りが少し短くなれば嬉しく思います。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-08-14
ライセンス表示に載せる依存は、lockfile の dev/prod では決まりません
AI ビルダーが書き出したプロジェクトの依存を実際に分類し、ライセンス表示に載せる範囲をどう決めたかの記録です。copyleft の検出結果と配布物の境界の食い違いを、本番プロジェクトの実データで確かめました。
開発ツール2026-07-04
「続きを読む」を出すべきか、テキストの実測で決める — Rork(Expo)の折りたたみ表示設計
商品説明やレビュー本文を3行で畳み「続きを読む」を出す実装で、短い文にまでトグルが付いてしまう問題を解決します。onTextLayout で実際の行数を測り、はみ出す時だけトグルを出す設計を、iOS と Android の差、展開アニメーション、文字サイズ拡大への配慮まで動くコードでまとめます。
開発ツール2026-06-30
Rork アプリにホーム画面長押しのクイックアクションを足す — 動的項目とコールド起動の取りこぼしを防ぐ
アイコン長押しで出るクイックアクションを Rork(Expo)アプリに実装する設計です。静的・動的項目の使い分け、iOS と Android の差、そしてコールド起動でアクションがルーター準備前に届いて取りこぼす問題を、保留と再生で防ぐ実装手順までまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →