Androidエミュレーター上の.NET MAUIアプリから、開発PCで動かすローカルHTTPS APIへ接続すると証明書エラーになることがあります。これは自己署名(開発用)証明書をエミュレーターが信頼しないのが主因です。原因の切り分けと、アプリを止めずに開発を進める現実的な対処をまとめます。
起きている問題を一度「文章」で固定する
証明書エラーは原因の幅が広く、状況が少しでもズレると解決策が噛み合わなくなります。まずは前提を整理し、何を「成功」とみなすかを明確にします。
| 項目 | 内容(今回の想定) | 補足 |
|---|---|---|
| アプリ | .NET MAUI(Visual Studio 2022) | C#コードからAPI呼び出し、さらにWebView内JavaScript(fetch等)からも呼び出す |
| 実行環境 | Androidエミュレーター | エミュレーターはPCとは別の端末。証明書ストアも別物 |
| API | 開発PC上で動作するローカルAPI(HTTPS) | 自己署名/開発用証明書(dotnet dev-certs 等)になりやすい |
| 困りごと | エミュレーターからAPIへアクセスすると証明書エラー | 「まずはブラウザで警告が出ない状態」を目指したいが、開発を止めたくない |
結論:この証明書エラーは多くの場合「想定通り」の挙動
.NET MAUIの開発で、Androidエミュレーター/iOSシミュレーターからローカルHTTPSの開発用APIに接続すると証明書エラーになるのは珍しくありません。根本原因は、ローカルAPIが使っているHTTPS証明書が自己署名(またはローカルCA発行)であり、Android/iOS側の信頼ストアに入っていないためです。
よく見かける代表例は次の通りです。
| プラットフォーム | 代表的なエラー | 読み替え |
|---|---|---|
| Android | java.security.cert.CertPathValidatorException | 信頼できる認証局(trust anchor)が見つからず、証明書チェーンを検証できない |
| iOS | NSURLErrorDomain | サーバー証明書が無効、または検証できない |
ここで重要なのは、「PCで信頼済みにした開発証明書」が、そのままエミュレーターにも信頼されるわけではないという点です。PCの証明書ストアと、エミュレーターの証明書ストアは別物なので、PC側でdotnet dev-certs https --trustを実行しても、エミュレーターのブラウザや端末側が自動的に信頼してくれるわけではありません。
最初に切り分ける:証明書エラーは「信頼」と「ホスト名」の2軸
「証明書エラー」の正体は、だいたい次のどちらか(または両方)です。やることが変わるので、先に分類しておくと迷子になりにくいです。
| 分類 | 典型的な原因 | ありがちな状況 | 対処の方向 |
|---|---|---|---|
| 信頼(Trust)の問題 | 自己署名/ローカルCAで、端末が発行元CAを信頼していない | 「信頼できない証明書」「trust anchorがない」 | 端末にCAを入れる、または開発時のみ検証をバイパスする |
| ホスト名(SAN)の不一致 | 証明書のSAN(Subject Alternative Name)に接続先ホストが入っていない | localhost用の証明書でhttps://10.0.2.2へアクセス | 接続先を揃える(adb reverse等)、またはSANにIP/ホストを含めた証明書を用意 |
特にエミュレーターでは、ホストPCへ接続するために10.0.2.2を使うことが多く、「localhost用の開発証明書」と相性が悪い(SAN不一致になりやすい)点が落とし穴です。
通信の基本:エミュレーターからホストPCへは 10.0.2.2 を使う
Androidエミュレーターでありがちなつまずきが、「エミュレーターのlocalhostはPCではない」という点です。エミュレーターは独立した端末なので、エミュレーター内でhttps://localhostに接続しても、基本的にはエミュレーター自身を指します。
ホストPC上のAPIに繋ぐ場合、一般的には次のように指定します。
- ホストPC上のAPI:
https://10.0.2.2:ポート/
さらに、APIが「127.0.0.1(localhost)のみで待ち受け」だと、IP経由の接続が届かないことがあります。Kestrel/IIS Expressの待受設定が原因のケースもあるので、次を確認します。
- APIが
0.0.0.0(全インターフェース)や、PCのLAN IPでもListenできているか - Windows Defenderファイアウォール等でポートがブロックされていないか
- ポート番号・プロトコル(HTTP/HTTPS)を取り違えていないか
開発を止めない解決策:アプリ側で「ローカルだけ」証明書検証をスキップする
「エミュレーター内のブラウザで警告を消す」ことは可能ですが、証明書とOSの信頼ストアを正しく揃える必要があり、手順が増えます。開発を前に進めたいなら、まずはアプリ側でローカル宛て通信だけ証明書検証をバイパスするのが現実的です。
この方法のルールは次の通りです。
- 開発ビルドだけで有効にする(
#if DEBUGや環境変数で制御) - ローカル宛てだけに限定する(
10.0.2.2、localhost、特定のLAN IPなど) - 本番では必ず正式なCA発行証明書を使い、検証を無効化しない
PC側の準備:開発用HTTPS証明書を信頼済みにする
ASP.NET Coreの開発証明書を使っている場合、まずは開発PC側で信頼済みにします(PC上のブラウザやツールでの疎通確認がしやすくなります)。
dotnet dev-certs https --trust
これは「PCが自分の開発証明書を信頼する」ための操作で、エミュレーターを直接信頼させるものではありません。ただし、PC側でHTTPSが不安定だと切り分けが崩れるので、先に整えておく価値があります。
MAUI(C#)側:HttpClientでローカルのみ検証をバイパスする
DIでHttpClientを登録しているケースを想定し、MauiProgram.csでハンドラを差し替える例です。
using System.Net.Security;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.Services.AddHttpClient("LocalApi", client =>
{
client.BaseAddress = new Uri("https://10.0.2.2:5001/");
client.Timeout = TimeSpan.FromSeconds(30);
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
#if DEBUG
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
// エラーが無いなら通常通り許可
if (errors == SslPolicyErrors.None) return true;
var uri = message?.RequestUri;
if (uri is null) return false;
// ローカル開発用途だけに限定(必ず条件を絞る)
var host = uri.Host;
var isLocal =
host == "10.0.2.2" ||
host == "localhost" ||
host.StartsWith("192.168.") ||
host.StartsWith("10.") ||
host.StartsWith("172.16.");
return isLocal;
};
#endif
return handler;
});
return builder.Build();
}
}
「とりあえず常にtrue」にすると、意図せず外部通信まで検証が無効になる危険があります。許可するホストは、プロジェクトに合わせてさらに厳密に絞り込んでください(例:10.0.2.2と特定のLAN IPだけ)。
ブラウザで警告が残っても、アプリが動けば開発として成立するケース
次の状態は“よくある”ので、必要以上に深追いしない判断も大切です。
- エミュレーター内ブラウザ(Chrome等)では、ローカルAPIにアクセスすると証明書エラーが出る
- 一方で、MAUIアプリからのAPI呼び出しは成功する(検証バイパスが効いている)
ブラウザはOS標準の信頼ストアに従って厳密に検証しますが、アプリはアプリ内でカスタムルールを持てます。目的が「アプリからローカルAPIを叩いて機能開発を進めること」なら、ブラウザの警告が残っていても致命的でない場合があります。
WebView(JavaScript)から呼ぶ場合は“別ルート”として考える
WebView内JavaScriptのfetchは、C#のHttpClient設定とは別に証明書検証されます。つまり、C#側で直ったのにWebViewだけ失敗という状態が起きえます。
WebViewで詰まりやすいポイント
- 証明書検証:WebViewはブラウザに近い挙動で検証する
- Androidの仕様:ユーザー追加CAはアプリが明示的に許可しないと信頼されないことがある
- CORS:証明書が通っても、APIがCORSヘッダーを返さないとJS側でブロックされる
選択肢:WebView側でもローカルだけSSLエラーを許可する(Android限定・開発限定)
どうしてもWebViewから直接ローカルHTTPS APIを叩きたい場合、AndroidではWebViewClientでSSLエラー時の挙動を制御できます。ただし、セキュリティ上の影響が大きいので、開発限定・ローカル限定に閉じ込めてください。
#if ANDROID
using Android.Webkit;
using Microsoft.Maui.Handlers;
public class LocalOnlySslBypassWebViewClient : WebViewClient
{
public override void OnReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
{
#if DEBUG
var url = error?.Url ?? string.Empty;
// 例:ローカルAPI(10.0.2.2)宛てだけ許可
if (url.StartsWith("https://10.0.2.2"))
{
handler.Proceed();
return;
}
#endif
handler.Cancel();
}
}
public static class WebViewSslBypass
{
public static void Apply()
{
WebViewHandler.Mapper.AppendToMapping("LocalOnlySslBypass", (handler, view) =>
{
handler.PlatformView.SetWebViewClient(new LocalOnlySslBypassWebViewClient());
});
}
}
#endif
この実装を採る場合、次の運用ルールを推奨します。
- デバッグビルドでのみ有効化する(リリースビルドに混入させない)
- 許可するURL条件を可能な限り厳密にする(文字列包含ではなくホスト一致に寄せる)
- 長期的には「正しい証明書を信頼させる」方向へ寄せる
より安全に寄せる:AndroidのNetwork Security ConfigでユーザーCAを信頼する
「検証を無効化」ではなく、「開発用CAを正しく信頼する」方法もあります。Android 7以降では、ユーザー追加CAはアプリが明示的に許可しないと信頼しない挙動があるため、デバッグ用にNetwork Security Configを用意します。
例として、Resources/xml/network_security_config.xmlを作り、デバッグ用途でユーザーCAを許可します。
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
そしてAndroidManifest側でandroid:networkSecurityConfigを指定します(MAUIではPlatforms/Android/AndroidManifest.xmlをカスタムする運用が一般的です)。この方法は「正しく信頼する」方向に寄せられるので、WebViewとの相性が良くなるケースがあります。
ブラウザでも証明書エラーを消したい場合の選択肢
エミュレーター内ブラウザの警告を消すには、エミュレーターがその証明書(または発行元CA)を信頼し、かつアクセス先ホスト名が証明書に一致する必要があります。つまり、やることは次の2点に集約されます。
- 信頼の確立:CA証明書をエミュレーターの信頼ストアへ入れる
- ホスト名の整合:SANに接続先(10.0.2.2等)が含まれる証明書を使う、または接続先を証明書に合わせる
| 方法 | 狙い | 向いているケース | 注意点 |
|---|---|---|---|
SANに10.0.2.2等を含めた証明書を使う | ホスト名不一致を解消 | 常に10.0.2.2でアクセスしたい | 証明書の作成・配布・更新が増える |
adb reverseでlocalhostに寄せる | 既存のlocalhost用証明書を活かす | 開発端末が固定、作業者が少ない | 接続前にadb設定が必要になることがある |
| 開発トンネル(ngrok等)を使う | 正式TLS(信頼済み)でアクセス | 複数人開発・端末多い・外出先検証 | 外部公開になるためアクセス制御が必須 |
| 開発時だけHTTPに切り替える | TLS自体を避ける | 最優先がスピード、短期検証 | 本番との差分が増える。Mixed Contentやセキュリティ要件に注意 |
「証明書を入れたのに直らない」パターンで多い原因
開発用証明書を作ってエミュレーターに入れても直らない場合、次のどれかに当たっていることが多いです。
- 入れたのがサーバー証明書:必要なのは「発行元CA(ルート/中間)」で、そこを信頼させないとチェーン検証に失敗する
- SAN不一致:localhost用証明書で
10.0.2.2へ接続している - アプリ側がユーザーCAを信頼しない:Android 7以降はNetwork Security Configが必要なことがある
- エミュレーターの時計ずれ:証明書の有効期限(NotBefore/NotAfter)で弾かれる
mitmproxyを試しても解決しにくい理由
mitmproxy等のHTTPSプロキシは、通信を中継して解析できる反面、クライアント側から見ると「別の証明書で終端している」状態になります。そのため、プロキシのCAを端末に信頼させない限り、結局は同じ“信頼問題”で詰まります。
さらに、アプリやWebViewが証明書ピンニングをしている場合、mitmproxyを挟むと通信が失敗することがあります。デバッグ手段としては有用ですが、「ローカルHTTPSの証明書エラーを根本から消す」目的では、証明書(信頼とSAN)を整えるか、開発限定のバイパスを入れる方が早く確実です。
本番に混入させないための運用ルール
証明書検証の無効化は、混入するとセキュリティ事故に直結します。次のルールで“混ぜない”設計にしておくと安心です。
| ルール | 具体例 | 狙い |
|---|---|---|
| コンパイル条件で封じる | #if DEBUGの中に閉じ込める | リリースビルドに入らない |
| 許可ホストを固定する | 10.0.2.2、特定LAN IPのみ | 意図しない外部通信の無効化を防ぐ |
| 設定でON/OFFできるようにする | アプリ設定や環境変数で切替 | デバッグ時だけ使う運用にしやすい |
| 本番は正式証明書 | Let’s Encrypt等の信頼済みCA | 端末・ブラウザ・WebViewで素直に通る |
まとめ
- AndroidエミュレーターからローカルHTTPS APIに接続すると証明書エラーになるのは、自己署名(開発用)証明書が信頼されないために起きる“想定内の挙動”であることが多い。
- 最初に「信頼(Trust)」と「ホスト名(SAN)」を切り分けると、遠回りが減る。
- 開発を止めないためには、MAUIアプリ側で開発時・ローカル宛てに限定して証明書検証をバイパスするのが現実的。
- WebView(JavaScript)から直接呼ぶ場合は別ルート扱い。必要ならWebView側の対処やNetwork Security Configも検討する。
- エミュレーター内ブラウザでも警告を消したいなら、CAの信頼とSAN整合(またはadb reverse等)という“正攻法”が必要。

コメント