Auto-Update Systems Expert について
Tauri アプリを中心に、安全で信頼性の高い自動更新システムを設計する開発者向けスキルです。署名検証、段階的リリース、ロールバック、配信方法を扱います。
主な機能
- 01暗号署名の検証
- 02ロールバックとバックアップの計画
- 03段階的なロールアウト戦略
- 04HTTPS による更新配信
- 05Tauri updater の設定
活用シーン
- 01Tauri アプリの更新フロー設計
- 02更新失敗時の復旧計画
- 03stable と beta の更新チャネル管理
- 04差分ダウンロードによる帯域削減
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ martinholovsky/ claude-skills-generator --skill auto-update-systems-expertインストール後、Claude Code で/auto-update-systems-expertを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/advanced-patterns.mdsecurity-examples.mdthreat-model.md詳細
自動更新システムの専門家
0. 必読プロトコル
重要:実装を始める前に、次のリファレンスファイルを読んでください。
| リファレンス | 読むタイミング |
|---|---|
references/security-examples.md | 署名鍵、署名検証、安全なエンドポイント |
references/advanced-patterns.md | 段階的ロールアウト、ロールバック、更新チャネル、差分更新 |
references/threat-model.md | セキュリティ対策、MITM 防御、鍵のローテーション |
1. 概要
リスクレベル:高
理由:自動更新システムは、すべてのユーザーに同時にコードを配布する可能性があります。更新システムが侵害されると、ユーザーベース全体にマルウェアが配布されるおそれがあります。署名検証の回避(CVE-2024-39698 など)により、攻撃者が署名されていない悪意のある更新をインストールできてしまいます。不適切なロールバックの仕組みは、ソフトウェアが壊れたままユーザーに残る原因となります。
自動更新システムの実装を専門とし、次の分野に精通しています。
- 署名検証による暗号学的な更新の完全性の確保
- 更新に失敗した場合のロールバックの仕組み
- リスク軽減のための段階的ロールアウト
- HTTPS とピンニングによる安全な配布
- Tauri updater の設定とベストプラクティス
主なユースケース
- Tauri アプリケーションの自動更新
- 安全な更新配布インフラストラクチャ
- 更新チャネルの管理(安定版、ベータ版)
- 緊急ロールバック手順
- 更新の分析と監視
2. 中核となる責務
2.1 中核となる原則
- まず TDD - 実装コードを書く前にテストを作成する
- パフォーマンスを考慮 - 帯域幅と速度を最適化する
- 必ず署名を検証する - 署名されていない更新は絶対にインストールしない
- HTTPS のみを使用する - HTTP 経由で更新を取得しない
- ロールバックを実装する - 更新に失敗した場合に備える
- 段階的にロールアウトする - すべてのユーザーを一度に更新しない
- 更新の健全性を監視する - 成功率とエラーを追跡する
2.2 信頼性の原則
- アトミックな更新 - インストールはすべて完了するか、まったく行われないかのどちらかにする
- ユーザーデータを保持する - 更新中に設定を失わない
- 段階的な機能縮退 - 更新に失敗してもアプリが動作するようにする
- ユーザーの同意 - 更新前にユーザーに知らせる
3. 技術的な基盤
3.1 Tauri Updater の構成要素
| 構成要素 | 目的 |
|---|---|
| 更新マニフェスト | バージョン、ダウンロード URL、署名を含む JSON |
| 署名鍵 | 更新への署名に使用する Ed25519 秘密鍵 |
| 公開鍵 | 検証用にアプリに埋め込む |
| 更新エンドポイント | マニフェストと成果物をホストする HTTPS サーバー |
3.2 バージョンに関する推奨事項
| 構成要素 | 推奨 | 備考 |
|---|---|---|
| Tauri | 1.5+ / 2.0+ | 最新のセキュリティパッチ |
| 更新プロトコル | v2 | 署名の処理がより適切 |
4. 実装パターン
4.1 Tauri Updater の設定
// tauri.conf.json
{
"tauri": {
"updater": {
"active": true,
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6...",
"endpoints": [
"https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
],
"windows": {
"installMode": "passive"
}
},
"bundle": {
"createUpdaterArtifacts": true
}
}
}
4.2 更新マニフェストの形式
{
"version": "1.2.0",
"notes": "Bug fixes and performance improvements",
"pub_date": "2024-01-15T12:00:00Z",
"platforms": {
"darwin-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
"url": "https://releases.myapp.com/MyApp_1.2.0_x64.app.tar.gz"
},
"windows-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
"url": "https://releases.myapp.com/MyApp_1.2.0_x64-setup.nsis.zip"
}
}
}
4.3 カスタム更新ロジック
use tauri::updater::UpdateResponse;
use tauri::{AppHandle, Manager};
#[tauri::command]
async fn check_for_updates(app: AppHandle) -> Result<Option<UpdateInfo>, String> {
match app.updater().check().await {
Ok(update) => {
if update.is_update_available() {
Ok(Some(UpdateInfo {
version: update.latest_version().to_string(),
notes: update.body().map(|s| s.to_string()),
date: update.date().map(|d| d.to_string()),
}))
} else {
Ok(None)
}
}
Err(e) => Err(format!("Failed to check for updates: {}", e)),
}
}
#[tauri::command]
async fn install_update(app: AppHandle) -> Result<(), String> {
let update = app.updater().check().await
.map_err(|e| format!("Check failed: {}", e))?;
if update.is_update_available() {
// Download and verify signature
update.download_and_install()
.await
.map_err(|e| format!("Install failed: {}", e))?;
// Restart app to apply update
app.restart();
}
Ok(())
}
#[derive(serde::Serialize)]
struct UpdateInfo {
version: String,
notes: Option<String>,
date: Option<String>,
}
5. セキュリティ基準
5.1 この分野の脆弱性の状況
調査日:2024年11月
| CVE | 深刻度 | 説明 | 緩和策 |
|---|---|---|---|
| CVE-2024-39698 | 高 | electron-updater の署名検証回避 | electron-builder 6.3.0+ に更新 |
| CVE-2024-24576 | 高 | Rust のコマンドインジェクション(Tauri shell に影響) | Rust 1.77.2+ に更新 |
| CVE-2024-35222 | 高 | Tauri の iFrame オリジン検証回避 | Tauri 1.6.7+/2.0.0-beta.20+ に更新 |
| CVE-2023-46115 | 中 | Vite の設定を介した Tauri キー漏えい | TAURI_ を envPrefix から削除 |
重要なポイント:署名検証の回避は、最も重大な脆弱性の種類です。署名が実際に検証されており、回避できないことを必ず確認してください。
5.2 OWASP との対応
| OWASP カテゴリ | リスクレベル | 主な対策 |
|---|---|---|
| A02:2021 - 暗号化の失敗 | 重大 | Ed25519 署名、HTTPS のみ |
| A05:2021 - セキュリティ設定のミス | 高 | 適切なエンドポイント設定、鍵管理 |
| A08:2021 - ソフトウェアの完全性の失敗 | 重大 | 署名検証、ピンニング |
5.3 署名の検証
完全な実装については、references/security-examples.md を参照してください
// Tauri handles signature verification automatically when configured correctly
// The signature in the manifest is verified against the embedded public key
// CRITICAL: Never bypass signature verification
// CRITICAL: Always use HTTPS for update endpoints
// CRITICAL: Protect the private signing key
6. テスト基準
6.1 更新のテスト
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_update_check() {
let mock_server = MockUpdateServer::new();
mock_server.set_latest_version("2.0.0");
let result = check_for_updates_from(&mock_server.url()).await;
assert_eq!(result.unwrap().version, "2.0.0");
}
#[tokio::test]
async fn test_invalid_signature_rejected() {
let mock_server = MockUpdateServer::new();
mock_server.set_invalid_signature();
assert!(install_update_from(&mock_server.url()).await.is_err());
}
#[tokio::test]
async fn test_downgrade_prevented() {
let mock_server = MockUpdateServer::new();
mock_server.set_latest_version("0.9.0");
assert!(check_for_updates_from(&mock_server.url()).await.unwrap().is_none());
}
}
7. 実装ワークフロー(TDD)
ステップ 1:最初に失敗するテストを書く
# tests/test_update_system.py
import pytest
from unittest.mock import patch
from update_manager import UpdateManager
class TestUpdateManager:
@pytest.fixture
def manager(self):
return UpdateManager(current_version="1.0.0", update_endpoint="https://updates.example.com")
@pytest.mark.asyncio
async def test_check_for_update_returns_info(self, manager):
with patch.object(manager, '_fetch_manifest') as mock:
mock.return_value = {"version": "2.0.0", "signature": "valid_sig"}
result = await manager.check_for_update()
assert result.version == "2.0.0"
@pytest.mark.asyncio
async def test_invalid_signature_rejected(self, manager):
with patch.object(manager, '_verify_signature', return_value=False):
with pytest.raises(SecurityError, match="signature"):
await manager.download_and_verify("https://...", "bad_sig")
@pytest.mark.asyncio
async def test_rollback_on_install_failure(self, manager):
with patch.object(manager, '_install', side_effect=InstallError):
with patch.object(manager, '_restore_backup') as mock_restore:
with pytest.raises(InstallError):
await manager.install_update("/path/to/update")
mock_restore.assert_called_once()
ステップ 2:テストに合格するための最小限の実装を行う
# update_manager.py
class UpdateManager:
async def check_for_update(self) -> Optional[UpdateInfo]:
manifest = await self._fetch_manifest()
if self._is_newer(manifest["version"]):
return UpdateInfo(**manifest)
return None
async def download_and_verify(self, url: str, signature: str) -> bytes:
data = await self._download(url)
if not self._verify_signature(data, signature):
raise SecurityError("Invalid signature")
return data
ステップ 3:リファクタリングと最適化
テストに合格したら、差分更新、キャッシュ、帯域幅管理を追加します。
ステップ 4:検証
pytest tests/test_update_system.py -v --tb=short
pytest tests/test_update_system.py --cov=update_manager --cov-report=term-missing
pytest tests/test_update_system.py -k "signature or rollback" -v
8. パフォーマンスパターン
8.1 差分更新
# Good: Download only changed bytes
class DeltaUpdateManager:
async def download_delta(self, from_version: str, to_version: str) -> bytes:
delta_url = f"{self.endpoint}/deltas/{from_version}-{to_version}.patch"
delta = await self._download(delta_url)
return self._apply_delta(self.current_binary, delta)
# Bad: Download full binary every time
class FullUpdateManager:
async def download_update(self, version: str) -> bytes:
return await self._download(f"{self.endpoint}/full/{version}.tar.gz")
8.2 バックグラウンドダウンロード
# Good: Download in background without blocking UI
class BackgroundDownloader:
async def download_in_background(self, url: str) -> None:
self._download_task = asyncio.create_task(self._download(url))
self._download_task.add_done_callback(self._on_download_complete)
def get_progress(self) -> float:
return self._bytes_downloaded / self._total_bytes
# Bad: Blocking download that freezes application
def download_blocking(url: str) -> bytes:
return requests.get(url).content # Blocks entire app
8.3 帯域幅の制限
# Good: Respect user's bandwidth limits
class ThrottledDownloader:
def __init__(self, max_bytes_per_sec: int = 1_000_000):
self.rate_limiter = RateLimiter(max_bytes_per_sec)
async def download(self, url: str) -> bytes:
chunks = []
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
async for chunk in response.content.iter_chunked(8192):
await self.rate_limiter.acquire(len(chunk))
chunks.append(chunk)
return b''.join(chunks)
# Bad: Saturate user's connection
async def download_unlimited(url: str) -> bytes:
async with aiohttp.ClientSession() as session:
return await (await session.get(url)).read()
8.4 ロールバックの最適化
# Good: Keep only necessary backup data
class SmartRollback:
def create_backup(self) -> BackupHandle:
# Only backup files that will be modified
modified_files = self._get_files_to_update()
return self._backup_files(modified_files)
def cleanup_old_backups(self, keep_count: int = 2) -> None:
backups = sorted(self._list_backups(), key=lambda b: b.date)
for backup in backups[:-keep_count]:
backup.delete()
# Bad: Full backup every time
class FullBackup:
def create_backup(self) -> str:
# Copies entire application directory
return shutil.copytree(self.app_dir, f"{self.app_dir}.backup")
8.5 署名のキャッシュ
# Good: Cache verified signatures
class CachedSignatureVerifier:
def __init__(self):
self._verified_cache: Dict[str, bool] = {}
def verify(self, data: bytes, signature: str) -> bool:
cache_key = hashlib.sha256(data).hexdigest()
if cache_key in self._verified_cache:
return self._verified_cache[cache_key]
result = self._verify_ed25519(data, signature)
self._verified_cache[cache_key] = result
return result
# Bad: Re-verify same data multiple times
class UncachedVerifier:
def verify(self, data: bytes, signature: str) -> bool:
return self._verify_ed25519(data, signature) # Expensive each time
9. よくある間違いとアンチパターン
| 間違い | 誤り | 正しい方法 |
|---|---|---|
| 署名がない | 設定に pubkey がない | アップデーターの設定には必ず pubkey を含める |
| HTTP エンドポイント | http://updates... | 必ず https://updates... を使用する |
| キーの漏えい | envPrefix: ['VITE_', 'TAURI_'] | envPrefix: ['VITE_'] のみを使用する(CVE-2023-46115) |
| ロールバックがない | バックアップせずにインストールする | インストール前にバックアップし、失敗した場合は復元する |
// CORRECT: Update with rollback
async fn update(&self) -> Result<(), UpdateError> {
let backup = self.backup_current_version()?;
if let Err(e) = self.try_update().await {
self.restore_from_backup(&backup)?;
return Err(e);
}
self.cleanup_backup(&backup)?;
Ok(())
}
10. 実装前チェックリスト
フェーズ 1:コードを書く前
- 更新チェック、署名検証、ロールバックについて、失敗するテストを書く
-
references/threat-model.mdの脅威モデルを確認する - 署名鍵の管理計画(生成、保管、ローテーション)を確認する
- ロールバック戦略とバックアップ対象の範囲を定義する
- 帯域幅の制限と差分更新のサポートを計画する
フェーズ 2:実装中
- 公開鍵をアプリの設定に埋め込む
- 秘密鍵を安全に保管する(CI のシークレットのみ)
- すべてのエンドポイントで HTTPS を使用する
- パフォーマンス向上のため、署名のキャッシュを実装する
- 進捗を追跡するバックグラウンドダウンロードを追加する
- アトミックな更新(すべて成功するか、何も反映されないか)を保証する
- 更新中もユーザーデータを保持する
フェーズ 3:コミット前
- すべてのテストに合格する:
pytest tests/test_update_system.py -v - 無効な署名を使って署名検証をテストする
- ダウングレード攻撃を防止する
- ロールバック機構をテストする
- ネットワーク障害のシナリオをテストする
- すべてのプラットフォームで更新をテストする
- コミットするコードにシークレットが含まれていない
- 鍵のローテーション手順を文書化する
11. まとめ
自動更新システムでは、次の点を実現することを目指します。
- 暗号学的に安全:更新のたびに Ed25519 署名を検証する
- 信頼性が高い:ロールバック機能を備えたアトミックな更新
- 使いやすい:明確な情報提供と、最小限の中断
自動更新システムは、次の理由から価値の高い標的となることを理解してください。
- すべてのユーザーに同時にコードを配布できる
- インストール中に高い権限で実行される
- ユーザーは、インストールしたアプリからの更新を信頼する
- 更新が侵害されると、ユーザーベース全体に影響が及ぶ
セキュリティに関する注意:署名検証を絶対に省略しないでください。HTTPS を必ず使用してください。署名用の秘密鍵を必ず保護してください。ロールバックを必ず実装してください。判断に迷った場合は、攻撃シナリオについて references/threat-model.md を参照してください。