社内の検証サーバーや手元で立てた HTTPS の API は、自己署名証明書(公的な認証局の署名が無い証明書)を使っていることが多く、そのまま接続すると証明書の検証で失敗します。検証を切れば通りますが、それは「通信相手が本物かを確かめない」ということです。同じネットワークにいる第三者が相手になりすまし、通信を盗み見たり書き換えたりできる状態(中間者攻撃)になります。このレシピでは、開発中に限って検証を切る方法と本番ビルドに残さない仕組み、検証を切らずに信頼させる本来の方法を説明します。
検証を切ったまま配布してはいけません。 検証を無効にしたクライアントは、期限切れの証明書も他人のサイトの証明書も受け入れます。社内専用のアプリでも、公衆無線 LAN などから使われれば攻撃の対象になります。
| 方法 | 証明書の検証 | 本番で使えるか |
|---|---|---|
開発用サーバーを http://localhost で動かす | TLS を使わない | 開発のみ |
| 自分用の認証局を OS に登録する(mkcert など) | 有効 | 開発のみ |
| 社内の認証局の証明書を Rust で追加する | 有効 | 使える |
検証を無効にする(danger 系の設定) | 無効 | 使えない |
手元の API なら、HTTPS をやめて http://localhost で動かすのが最も簡単です。http プラグインや Rust からの通信は http:// でもそのまま送れます。
前提条件
npm run tauri add http
JS から検証を切る danger オプションは、http プラグインを Cargo の dangerous-settings 機能付きでビルドしたときだけ使えます。依存関係に直接書くと本番ビルドにも入るので、アプリ側に開発用の機能を作り、その中で有効にします。
[dependencies]
tauri-plugin-http = "2"
reqwest = "0.12"
[features]
# 開発時だけ --features insecure-dev-tls で有効にする
insecure-dev-tls = ["tauri-plugin-http/dangerous-settings"]
npm run tauri dev -- --features insecure-dev-tls
本番用にアプリをビルドする ときは --features を付けないので、この機能は配布物に入りません。証明書の検証を切っても URL の許可リストは有効なままなので、開発用サーバーの URL は http:default の allow に書きます(HTTP GET リクエストを送る(Rust経由))。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "http:default",
"allow": [{ "url": "https://localhost:8443/*" }]
}
]
}
1. フロントエンドから実装する (TypeScript)
import { fetch } from '@tauri-apps/plugin-http';
const DEV_API = 'https://localhost:8443';
/** 開発用サーバーに送る。検証を切るのは Vite の開発サーバーで動いているときだけ */
export async function fetchDevApi(path: string, init: RequestInit = {}): Promise<Response> {
return fetch(new URL(path, DEV_API), {
...init,
...(import.meta.env.DEV ? { danger: { acceptInvalidCerts: true, acceptInvalidHostnames: false } } : {}),
});
}
acceptInvalidCerts は署名や有効期限を含む検証をすべて省き、acceptInvalidHostnames は証明書の名前と接続先の一致だけを省きます。danger はそのリクエストだけに効き、ほかの fetch() は通常どおり検証されます。
この書き方は 2 重の安全策になっています。import.meta.env.DEV は vite build で false に置き換わるので、本番の JS は danger を渡しません。それでも何かの手違いで渡された場合、dangerous-settings が無いビルドでは「dangerous settings used but are not enabled」で失敗し、検証なしで通ることはありません。
2. バックエンドから実装する (Rust)
開発中だけ検証を切るクライアント
tauri::is_dev() は tauri dev で起動したときだけ true で、tauri build では --debug を付けても false です(#[cfg(debug_assertions)] との違いは 開発サーバーの起動とデバッグ)。これで検証を切る条件を包み、さらに送り先を開発用サーバーに限ります。reqwest のエラーは表示用の文字列に原因が含まれないので、source() をたどって証明書の問題かどうか分かるようにしておきます。
/// 開発用サーバー向けのクライアント。tauri dev で起動したときだけ証明書の検証を切る
fn dev_server_client() -> reqwest::Result<reqwest::Client> {
let builder = reqwest::Client::builder();
let builder = if tauri::is_dev() {
builder.danger_accept_invalid_certs(true)
} else {
builder
};
builder.build()
}
/// base と同じオリジンの URL だけを作る("//other.example" などで外へ出さない)
fn url_on(base: &str, path: &str) -> Result<reqwest::Url, String> {
let base = reqwest::Url::parse(base).map_err(|e| e.to_string())?;
let url = base.join(path).map_err(|e| e.to_string())?;
if url.origin() != base.origin() {
return Err(format!("unexpected origin: {url}"));
}
Ok(url)
}
/// エラーの原因をつなげて 1 行にする(証明書の問題は原因の側に書かれている)
fn error_chain(e: &dyn std::error::Error) -> String {
let mut msg = e.to_string();
let mut cur = e.source();
while let Some(s) = cur {
msg.push_str(": ");
msg.push_str(&s.to_string());
cur = s.source();
}
msg
}
#[tauri::command]
async fn fetch_dev_api(path: String) -> Result<String, String> {
let url = url_on("https://localhost:8443/", &path)?; // 検証を切るのは開発用サーバーだけ
let client = dev_server_client().map_err(|e| e.to_string())?;
let res = client.get(url).send().await.map_err(|e| error_chain(&e))?;
res.text().await.map_err(|e| error_chain(&e))
}
// 開発用の機能を付けたまま最適化ビルドをしたら、ビルドを止める
#[cfg(all(feature = "insecure-dev-tls", not(debug_assertions)))]
compile_error!("insecure-dev-tls は開発専用です。--features から外してください");
検証を切らずに自己署名証明書を信頼させる
開発用なら、mkcert のような道具で自分専用の認証局を作って OS に登録し、その認証局で localhost の証明書を発行するのが確実です。
mkcert -install # 自分専用の認証局を作り、OS に登録する
mkcert localhost 127.0.0.1 # その認証局で localhost 用の証明書と鍵を作る
作った証明書を開発用サーバーに設定すると、OS の証明書ストアで検証する通信(WebView 自身の通信など)は検証を通ります。http プラグインは既定では同梱のルート証明書で検証するため、OS に登録した認証局が使われないことがあります。その場合は tauri-plugin-http に rustls-tls-native-roots 機能を付けると、OS の証明書ストアも参照します。mkcert が作った認証局の鍵は、ほかの人に渡さないでください。
社内サーバーのように本番でも自前の認証局を使うなら、その認証局の証明書をアプリに同梱し、reqwest の add_root_certificate() で信頼に加えます。検証は有効のままで、ホスト名の確認も行われます。同梱には tauri.conf.json の bundle.resources を使います(画像やDBファイルを配布物に同梱する)。
{
"bundle": {
"resources": ["certs/internal-ca.pem"]
}
}
use tauri::{path::BaseDirectory, Manager};
/// 同梱した社内認証局の証明書を信頼に加えたクライアント。検証は有効のまま
fn internal_client(app: &tauri::AppHandle) -> Result<reqwest::Client, String> {
let path = app
.path()
.resolve("certs/internal-ca.pem", BaseDirectory::Resource)
.map_err(|e| e.to_string())?;
let pem = std::fs::read(&path).map_err(|e| format!("{}: {e}", path.display()))?;
let ca = reqwest::Certificate::from_pem(&pem).map_err(|e| e.to_string())?;
reqwest::Client::builder()
.add_root_certificate(ca) // この認証局が発行した証明書も信頼する
.build()
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn fetch_internal(app: tauri::AppHandle, path: String) -> Result<String, String> {
let url = url_on("https://intranet.example.local/", &path)?;
let client = internal_client(&app)?;
let res = client.get(url).send().await.map_err(|e| error_chain(&e))?;
res.text().await.map_err(|e| error_chain(&e))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_http::init())
.invoke_handler(tauri::generate_handler![fetch_dev_api, fetch_internal])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
例では呼ぶたびにクライアントを作っていますが、実際は setup で 1 つ作って使い回します(通信のタイムアウト時間を設定する)。
import { invoke } from '@tauri-apps/api/core';
console.log(await invoke<string>('fetch_dev_api', { path: '/api/health' }));
console.log(await invoke<string>('fetch_internal', { path: '/api/status' }));
動作確認
自己署名証明書で https://localhost:8443 にサーバーを立て、npm run tauri dev -- --features insecure-dev-tls で起動して fetchDevApi('/api/health') を呼ぶと応答が返ります。--features を付けずに起動すると 1 行目、danger を渡さずに呼ぶと 2 行目のエラーになります。
dangerous settings used but are not enabled
error sending request for url (https://localhost:8443/api/health)
npm run tauri build で作ったアプリでは fetch_dev_api も検証を行うので、自己署名証明書のサーバーには接続できません。
よくあるエラーと対処法
- 「dangerous settings used but are not enabled」:
dangerを渡しましたが、http プラグインがdangerous-settings無しでビルドされています。開発時は--features insecure-dev-tlsを付けて起動します。本番で出るならimport.meta.env.DEVの条件が抜けています。 - 「error sending request for url (https://localhost:…)」: 証明書の検証に失敗した場合もこの文言で、JS には原因が出ません。Rust で
source()をたどると、認証局が不明、名前が一致しない、などの原因が分かります。 - 「url not allowed on the configured scope: https://localhost:8443/…」: 検証を切っても許可リストは有効です。
allowに開発用サーバーを足します。 - mkcert の証明書で WebView は通るのに http プラグインだけ失敗する: 同梱のルート証明書で検証しています。
rustls-tls-native-roots機能を付けるか、Rust のadd_root_certificate()を使います。
OS ごとの違いと注意点
- WebView の通信には効かない:
dangerやdanger_accept_invalid_certs()は、http プラグインと reqwest の通信だけに効きます。WebView が読み込むページ(HTTPS の開発サーバーなど)や標準のfetch()は OS 側の判定に従うので、mkcert などで OS に信頼させます。 - Windows: Windows でだけ効く
additionalBrowserArgsで WebView に起動引数を渡し、証明書エラーを無視させる(--ignore-certificate-errors)方法が知られています。アプリ内のすべての通信で検証が止まるうえ、既定で渡されている引数も置き換わるので勧めません。 tauri build --debug: 上のcompile_error!は働きませんが、--featuresを付けなければdangerous-settingsは入らず、is_dev()もfalseです。
