開発用に SSL 証明書エラーを無視する

http プラグインの danger オプションと reqwest の danger_accept_invalid_certs で自己署名証明書を通す。本番ビルドに残さない仕組みと、検証を切らずに証明書を信頼させる方法も示す。

通信 対象: Tauri 2.x 更新日: 読了目安: 約10分 net-008
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 2. バックエンドから実装する (Rust)
  4. 開発中だけ検証を切るクライアント
  5. 検証を切らずに自己署名証明書を信頼させる
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

社内の検証サーバーや手元で立てた 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 です。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する