更新履歴(6件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- 発行元を固定するために組んでいた`X509Chain`が、現在時刻で有効期間を再判定していたのを直しました。`X509ChainPolicy.VerificationTime`の既定はコンストラクターを呼んだ時刻なので、署名証明書が期限切れになった時点で`NotTimeValid`で失敗します。タイムスタンプ付きの署名は期限切れ後も`Status = Valid`のままなので、この形では「タイムスタンプが効いて有効と判定されたパッケージを、そのすぐ後ろで期限切れとして弾く」ことになり、証明書を更新した瞬間に過去のリリースが全部落ちます。有効期間と信頼の判定は`Get-AuthenticodeSignature`で済んでいて、この`Build`は発行元を調べるためだけのものなので、`IgnoreNotTimeValid`を指定する形にしました(署名時刻が分かるなら`VerificationTime`に入れます)。同じ理由で失効確認もここでは行わず、鍵漏洩時に止める手段はblocklistとminimum allowed versionであることを明記しています。
- Authenticode検証の例で、署名者の固定を`Subject`(識別名)の一致だけで行っていたのを直しました。識別名は一意な識別子ではなく、同じCN/O/Cを持つコード署名証明書は企業内CAでも公開CAでも発行できます。その CA をクライアントが信頼していれば、別の鍵・別の発行元で署名された差し替え版が`Status = Valid`かつ`Subject`一致で通ります。`X509Chain`を組んで期待する発行元(中間CA/ルート)のサムプリントが経路に含まれることを確認し、`Subject`はその上での絞り込みに使う形にしました。あわせて、期待値を配列で持って新旧を並べて受け付ける切り替え手順を明記し、チェックリストにも2項目追加しています。
- TUFの正式名称を併記し、脅威と対策の対応表(TLSだけで防げるかの列を含む)を新設しました。署名付きメタデータの最小JSON例と、検証前にURLを読まない順序、正規化を決めないと検証が食い違う点を追加しました。Authenticode署名をクライアントでどう検証するかの節(`Status=Valid`は誰の署名かを保証しないことを含む)、用語表、信頼の連鎖の図も追加しました。
- 本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589766)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「自動アップデートのセキュリティ設計 - HTTPSだけでは足りない理由」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589766 https://staging.comcomponent.com/blog/2026/04/09/000-comcomponent-autoupdate-security/
- DOI(最新版)
- 10.5281/zenodo.21589766
- DOI(この版)
- 10.5281/zenodo.21732783
目次
- まず結論
- なぜ自動アップデートは危険領域なのか
- ダメなパターン
- ベストプラクティス
- 最小安全構成
- Windows 案件ではどう考えるか
- 最低限のチェックリスト
- まとめ
- 参考資料
この記事の知識マップ
この記事は、自動アップデート機能の安全性はTLSだけでは確保できず、クライアント側のtrust anchorと署名付きmetadataの検証によって成り立つと整理します。TUFが定義するrollback・freeze・mix-and-match攻撃には、最高既知バージョンの保持や有効期限、hash・size・versionの固定といった対策が有効で、検証失敗時はfail-openではなくfail-closedにすることが推奨されます。Windowsでは要件が合えばMSIX App InstallerやClickOnceを優先し、自前updaterを作る場合もAuthenticode署名の検証、発行元チェーンの固定、タイムスタンプ付き署名、鍵の分離運用、helperへの権限分離、kill switchやblocklistによる事故対応までを含めて設計する必要があるとしています。
flowchart LR
accTitle: 自動アップデートのセキュリティ設計の知識マップ
accDescr: 自動アップデートがsigned metadataとtrust anchorを前提にrollback・freeze・mix-and-match攻撃をどう防ぎ、MSIX/ClickOnceとの使い分けや鍵管理・権限分離とどうつながるかを示す図です。
auto_updater["自動アップデート機能(updater)"]
signed_metadata["署名付き更新metadata"]
update_trust_anchor["updateのtrust anchor(信頼の起点)"]
public_key_cryptography["公開鍵暗号"]
mix_and_match_attack["mix-and-match攻撃(更新)"]
freeze_attack["freeze攻撃(更新)"]
minimum_allowed_version["minimum allowed version"]
rollback_attack["rollback攻撃(更新)"]
tuf["TUF(The Update Framework)"]
fail_closed["fail-closed設計"]
verification_failure_response["検証失敗時の挙動"]
fail_open["fail-open設計"]
msix_app_installer["MSIX + App Installer"]
windows_app_update_distribution["Windowsアプリの更新配布"]
clickonce["ClickOnce"]
code_signing_cert["コード署名証明書"]
get_authenticodesignature["Get-AuthenticodeSignature"]
winverifytrust["WinVerifyTrust"]
issuer_chain_pinning["発行元チェーンの固定"]
certificate_chain["証明書チェーン"]
thumbprint["拇印(サムプリント)"]
code_signing_timestamp["コード署名のタイムスタンプ"]
certificate_expiry["証明書の期限切れ"]
signing_key_separation["署名鍵の分離運用"]
hsm_key_storage["HSM/トークンでの秘密鍵保管"]
helper_privilege_separation["updater権限分離(helperへの分離)"]
kill_switch["kill switch(更新の緊急停止)"]
update_blocklist["更新のblocklist"]
auto_updater -.->|"前提とする"| update_trust_anchor
auto_updater -->|"前提とする"| signed_metadata
signed_metadata -->|"利用する"| public_key_cryptography
signed_metadata -->|"防止する"| mix_and_match_attack
signed_metadata -->|"防止する"| freeze_attack
minimum_allowed_version -->|"防止する"| rollback_attack
auto_updater -.->|"利用する"| tuf
fail_closed -->|"推奨される対応"| verification_failure_response
fail_open -->|"用いるのは非推奨"| verification_failure_response
msix_app_installer -->|"推奨される対応"| windows_app_update_distribution
clickonce -->|"推奨される対応"| windows_app_update_distribution
auto_updater -.->|"前提とする"| code_signing_cert
code_signing_cert -->|"で確認できる"| get_authenticodesignature
auto_updater -.->|"利用する"| winverifytrust
issuer_chain_pinning -->|"前提とする"| certificate_chain
issuer_chain_pinning -->|"利用する"| thumbprint
code_signing_timestamp -->|"軽減する"| certificate_expiry
code_signing_timestamp -->|"推奨される対応"| code_signing_cert
auto_updater -.->|"前提とする"| signing_key_separation
signing_key_separation -->|"利用する"| hsm_key_storage
helper_privilege_separation -->|"推奨される対応"| auto_updater
auto_updater -->|"前提とする"| minimum_allowed_version
auto_updater -->|"前提とする"| kill_switch
auto_updater -->|"前提とする"| update_blocklist
auto_updater -.->|"前提とする"| issuer_chain_pinning
windows_app_update_distribution -.->|"推奨される対応"| auto_updater
auto_updater -.->|"前提とする"| fail_closed
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全27件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
1. まず結論
先に、実務での落としどころを並べておきます。
- 要件が合うなら、まず MSIX App Installer や ClickOnce など既存の更新基盤を優先する
- 自前 updater が必要なら、最初に入れるべきは UI ではなく署名検証と失敗時復旧
latest.jsonのような更新情報は、未署名の設定ファイルではなく署名された metadata として扱う- TLS は必要だが十分条件ではない
- 更新判定は「サーバーがそう言っているから」ではなく、「クライアントが検証して正しいと判断できたから」にする
- 署名鍵は開発用と本番用を分離し、HSM や署名サービスで保護する
- 更新失敗時は fail-open ではなく fail-closed にする
- ロールバック対策がない updater は、脆弱版へ戻される前提で考えたほうが安全
- 署名検証をまだ入れられない段階なら、自動更新より手動の署名済み installer 配布のほうが安全
用語だけ、先に固定しておきます。以降はこの意味で使います。
| 用語 | 意味 |
|---|---|
| fail-closed | 異常時は「止まる側」へ倒す設計。署名検証に失敗したら更新を進めない |
| fail-open | 異常時でも「進む側」へ倒す設計。警告だけ出して更新を続けてしまう形 |
| staging | 新版をいったん別の場所へ展開しておき、検証が終わってから切り替える方式 |
| kill switch | 配信中の更新を、サーバー側の操作で即座に止める仕組み |
| trust anchor | クライアントが最初から信頼している起点。root 公開鍵や、固定した証明書チェーン |
要するに、自動アップデートの核心は「どうダウンロードするか」ではなく、「何を信頼し、どこで検証し、壊れたときにどう戻すか」です。
2. なぜ自動アップデートは危険領域なのか
通常の機能は、アプリの中に閉じています。 一方、updater は次の 3 つを一気に持ちます。
- 外部からファイルを取りに行く
- そのファイルを信頼する
- 既存の実行物を置き換える
つまり、任意コード実行の経路が製品の中に最初から組み込まれている、ということです。
ここでよくある誤解が、「HTTPS だから安全」というものです。 もちろん TLS は必要です。ただ、それで守れるのは主に通信経路と接続先の正当性です。更新サーバーそのものが侵害された、誤った成果物が正規 CDN へ置かれた、未署名の manifest が差し替えられた、という話には、それだけでは足りません。
実際、TUF(The Update Framework。ソフトウェア更新の信頼モデルを定義した仕様)が整理している脅威だけ見ても、更新系にはこれだけのものがあります。
- 任意の不正ソフトを入れさせる
- 脆弱な古い版へ戻させる rollback
- 新版を見せない freeze
- 互いに整合しない metadata と成果物を混ぜる mix-and-match
つまり、自動アップデートは「ファイル転送」ではなく「信頼の配布」です。 ここを設計して初めて、自動更新が安全に回り始めます。
2.1 脅威と対策の対応表
脅威と対策は、この記事では別の章に分かれています。先に対応関係だけ 1 枚にしておきます。設計の骨格はこれで足ります。
| 脅威 | 何が起きるか | TLS だけで防げるか | 主な対策 | 詳しい章 |
|---|---|---|---|---|
| 不正な成果物の配布 | 攻撃者の用意したファイルを正規更新として入れさせられる | 防げない(origin 侵害、誤配信に無力) | 署名付き metadata と、成果物の hash / 署名をクライアントで検証 | 4.2 / 4.3 / 4.4 |
| rollback | 署名は正しいが、既知の脆弱性がある旧版へ戻される | 防げない(署名も TLS も正しい) | 既知の最高 release version を保持し、それより古いものを拒否 | 4.8 |
| freeze | 新版が出ているのに、古い metadata を返し続けて更新させない | 防げない | metadata に expires_at を持たせ、古すぎるものを拒否 |
4.3 / 4.8 |
| mix-and-match | 整合しない metadata と成果物の組み合わせを渡される | 防げない | manifest に対象 artifact の hash / size / version を固定 | 4.3 / 4.8 |
| 署名鍵の侵害 | 正規署名付きの不正更新を配られる | 防げない | 開発 / 本番の鍵分離、HSM や署名サービス、承認フローと監査ログ、root と metadata 鍵の分離 | 4.5 |
| 更新の失敗・中断 | 置換の途中で落ちて、アプリが起動しなくなる | 対象外 | staging + atomic activate + rollback | 4.6 |
| 検証の迂回 | skipVerify のような抜け道が本番に残る |
対象外 | fail-closed を仕様として固定し、迂回フラグを持たない | 3.7 / 4.6 |
| 事故時に止められない | 問題のある版が配られ続ける | 対象外 | blocklist、minimum allowed version、kill switch | 4.8 / 7 |
「TLS だけで防げるか」の列が全部「防げない」か「対象外」になっているのが、この記事の趣旨です。TLS は経路を守りますが、配るものの中身と、配る権限を守るのは別の仕組みです。
3. ダメなパターン
先に、実務でよく見る危ない形をまとめます。
| ダメなパターン | 何が危ないか | 最低限の直し方 |
|---|---|---|
version.json を HTTPS で取り、URL の zip / exe をそのまま実行 |
origin 侵害、設定差し替え、誤配信に弱い | 署名付き metadata と成果物のクライアント検証に変える |
| バイナリだけ署名し、manifest は未署名 | URL、version、channel、必須更新フラグを改ざんできる | version / hash / size / channel / expiry を含む signed manifest にする |
| 署名鍵を開発 PC や CI のファイルに置く | 侵害されると正規署名付きマルウェアを配られる | HSM / 署名サービス + 承認フロー + 監査ログ |
| 更新失敗時に「検証エラーを無視して続行」 | 事故時に一番弱い経路が開く | fail-closed にする |
| 旧版を残さず上書き更新 | 電源断、ディスク不足、途中失敗で起動不能 | staging + atomic activate + rollback |
| バージョン比較だけで古い版を許す | 脆弱版への rollback が通る | 単調増加する release version と最高既知版の保存 |
| updater 全体を管理者権限で動かす | 侵害時の被害範囲が広い | ダウンロード/検証は低権限、置換だけ最小 helper へ分離 |
| 差分更新から始める | 実装が複雑で検証漏れが増える | まずはフルパッケージ更新から始める |
以下、少し詳しく見ます。
3.1 HTTPS だから大丈夫、で止まる
これは一番多いです。
- 起動時に
latest.jsonを読む downloadUrlを取り出す- zip / exe を落とす
- 展開して差し替える
- 終了
見た目はそれっぽいですが、信頼の根がサーバー返答に寄りすぎています。 更新サーバーや配信設定が侵害されたら、正しい HTTPS の上で不正な更新を配れてしまいます。
TLS は必要です。 ただし、TLS だけでは updater の設計は終わりません。
3.2 署名はしているが、クライアント側で検証していない
リリース時にファイルへ署名していても、クライアントがそれを見ていなければ意味がありません。
よくあるのは、
- CI では署名している
- でも updater は hash しか見ていない
- しかもその hash 自体が未署名 manifest から来る
という形です。
これだと、manifest を差し替えられた時点で、hash も一緒にすり替えられます。 「hash を見ているから安全」は、hash の出どころまで守れて初めて成立します。
3.3 manifest が未署名
更新系で本当に守るべきなのは、実行ファイルそのものだけではありません。 少なくとも次の情報は、改ざんされると危険です。
- version / release id
- ダウンロード対象の URL やファイル名
- hash / size
- channel(stable / beta など)
- 必須更新かどうか
- 適用可能な OS / アーキテクチャ
- metadata の有効期限
- 最低必要 updater version
つまり、更新判断に使う情報は全部 signed metadata に入れる くらいの感覚がちょうどいいです。
3.4 署名鍵の扱いが雑
更新機能の安全性は、かなりの割合で鍵管理の安全性です。
本番署名鍵が次のように置かれているなら、かなり危険です。
- 開発 PC の証明書ストアに入れっぱなし
- CI の secret として
.pfxをアップロード - 複数人が同じ秘密鍵をローカルへ配布
- 開発用署名と本番用署名が同じ信頼鎖
これだと、updater 自体が正しくても「正規署名された不正更新」を止められません。
3.5 旧版を残さない上書き更新
更新は、成功時より失敗時の設計が大事です。
- ダウンロード途中で切れた
- 展開に失敗した
- 置換途中で電源断した
- 新版は起動したが初回 migration で落ちた
このとき、旧版が消えていると復旧が重くなります。 実務では「更新に失敗した」という事実より、「現場でアプリが起動しなくなった」のほうが問題になります。
3.6 rollback を考えていない
署名された正規版でも、古い脆弱版なら攻撃者にとって都合がいいことがあります。
たとえば、
- version 1.8 に既知脆弱性がある
- 現場は 2.3 へ上がっている
- 攻撃者が 1.8 を再配信する
これが通ると、署名自体は正しいのに危険です。
「署名されているか」だけではなく、「その版を今入れてよいか」まで見ないと足りません。
3.7 fail-open
本番で一番やってはいけないのがこれです。
- 署名検証に失敗したら警告だけ出して続行
- 証明書の期限エラーを無視できる hidden flag がある
- デバッグ用の
skipVerify=trueが本番でも残る
障害時や攻撃時ほど、こういう抜け道が本命になります。
4. ベストプラクティス
4.1 まずは既存の更新基盤に乗る
自前 updater が本当に必要かは、最初に疑ったほうが安全です。
Windows なら、要件が合う限りはこのあたりを優先検討しやすいです。
- MSIX + App Installer
- ClickOnce
- Store / MDM / 社内配布基盤
- MSI + 企業側の配布管理
理由は単純で、更新そのものの責任範囲をある程度プラットフォームへ寄せられるからです。 もちろん自由度は下がりますが、更新 UI、配布 manifest、パッケージ署名、運用との整合が取りやすくなります。
自前 updater が必要になるのは、たとえばこんな時です。
- stable / beta / preview の複数チャネルを厳密に制御したい
- 段階配信や rollout 率を持ちたい
- 独自の業務都合で更新タイミングを細かく制御したい
- MSIX / ClickOnce に乗らない構成がある
この場合でも、「自由度が欲しい」ではなく「更新責任を自分たちで持つ」と理解したほうがぶれません。
4.2 信頼の起点をクライアント側へ持つ
安全な updater は、サーバー返答をそのまま信用しません。 クライアント側に、少なくとも次の 2 つが必要です。
- 信頼する公開鍵や証明書チェーン
- その鍵で署名された metadata を検証する仕組み
言い換えると、「サーバーが最新版と言っている」ではなく、 「この metadata は、信頼している署名者が出した最新版だ」とクライアントが確認できる状態を作る必要があります。
信頼が root からファイルまでどうつながるかを、図にしておきます。
flowchart TD
ROOT["root 鍵<br/>クライアントに埋め込む trust anchor"] --> SIGNKEY["metadata 署名鍵<br/>root から委譲し、頻繁に更新する"]
SIGNKEY --> META["署名付き update metadata<br/>version / url / hash / size / expiry"]
META --> CHECK1{"署名・有効期限・version を検証"}
CHECK1 -- "NG" --> STOP["更新を中止<br/>fail-closed"]
CHECK1 -- "OK" --> DL["成果物を staging 領域へダウンロード"]
DL --> CHECK2{"size / hash / パッケージ署名を検証"}
CHECK2 -- "NG" --> STOP
CHECK2 -- "OK" --> ACT["旧版を残したまま activate"]
ACT --> HEALTH{"初回起動の健全性確認"}
HEALTH -- "NG" --> RB["rollback して旧版へ戻す"]
HEALTH -- "OK" --> DONE["更新完了"]
図で押さえてほしいのは 2 点です。上から下へ、信頼が 1 本の鎖でつながっていること。そして、鎖のどこで切れても行き先が 更新を中止 か rollback であって、とりあえず進む にはならないことです。
4.3 signed metadata を中心に設計する
最低限、更新 metadata には次を入れて署名対象にします。
| 項目 | 入れる理由 |
|---|---|
| release version / release id | rollback 防止、監査 |
| artifact 名、URL、package type | どのファイルを取るかを固定する |
| hash、size | 改ざん検知、壊れた配信の検知 |
| channel | stable に beta を混ぜない |
| 対象 OS / architecture | 誤配布防止 |
| minimum updater version | protocol 変更時に古い updater を止める |
| expires_at | freeze 対策 |
| published_at | 監査、切り分け |
| mandatory / optional | 更新 UX の分岐も改ざん不可にする |
ここで大事なのは、更新の判断材料を全部 signed metadata に集約する ことです。 ロジックはクライアントにあり、情報の真正性は署名で守る、という形に寄せると事故が減ります。
形が見えないと実装に落ちないので、最小の例を書いておきます。まず、署名の対象になる中身です。
{
"schema_version": 1,
"channel": "stable",
"release_version": "2.4.1",
"published_at": "2026-04-09T01:00:00Z",
"expires_at": "2026-04-16T01:00:00Z",
"minimum_updater_version": "2.0.0",
"minimum_allowed_version": "2.2.0",
"mandatory": false,
"artifacts": [
{
"os": "windows",
"arch": "x64",
"package_type": "msi",
"file_name": "MyApp-2.4.1-x64.msi",
"url": "https://updates.example.com/stable/MyApp-2.4.1-x64.msi",
"size": 48234496,
"sha256": "5f2c...64桁の16進..."
}
]
}
これを、署名と一緒に包みます。
{
"signed": {
"schema_version": 1,
"channel": "stable",
"release_version": "2.4.1",
"_comment": "上のオブジェクトをそのまま入れる"
},
"signatures": [
{
"keyid": "3f9a...",
"sig": "MEUCIQ..."
}
]
}
この形にする理由は、更新判断に使う値が全部 signed の内側にある からです。URL も version も channel も必須更新フラグも内側なので、外側だけ差し替えても検証で落ちます。クライアント側の処理順は「signed を検証する → 通ったら、その中の値だけを使う」に固定します。検証前に url を読んでダウンロードを始めてしまう実装は、この形にした意味がなくなります。
実装で 1 つ注意があります。JSON は、キーの並び順や空白の入れ方でバイト列が変わります。署名検証はバイト列に対して行うため、署名対象を「正規化した表現」にするのか、「受信したバイト列そのもの」にするのか を先に決めてください。ここを決めずに、サーバー側は生成したオブジェクトを、クライアント側は再シリアライズした結果を署名対象にすると、正しい更新なのに検証に失敗します。逆に、この不一致を「まあ通してよいことにしよう」と回避し始めると、fail-open への第一歩になります。
4.4 成果物そのものも検証する
metadata を検証したあと、ダウンロードした成果物でも次を確認します。
- size
- hash
- パッケージ署名 / コード署名
- 発行元や期待する識別子
Windows の PE / MSI / MSIX を扱うなら、AuthentiCode やパッケージ署名の検証をクライアント側で行う前提にしたほうが安全です。 macOS なら Developer ID と notarization を更新経路でも前提にしたほうがぶれません。
4.5 鍵は機能ではなく運用で守る
鍵管理は、実装より運用で差が出ます。
最低でも次は分けたほうが安全です。
- 開発用署名鍵
- ステージング用署名鍵
- 本番用署名鍵
さらに本番用は、
- HSM
- クラウド署名サービス
- 承認フロー付きの signing system
- 監査ログ
- key rotation 手順
- timestamp 付き署名
まで含めて設計したいです。
「本番ビルドが通れば CI が自動署名する」は便利ですが、侵害時の被害半径も大きくなります。 少なくとも、誰が何をいつ署名したかは追えるようにしておくべきです。
運用が乗ってきたら、滅多に変えない root trust と、頻繁に再署名する更新 metadata の鍵を分けるとさらに安全です。 root を offline 寄りに保ち、更新 metadata には別鍵を使う設計は、鍵侵害時の被害半径を下げやすくなります。
4.6 fail-closed と staged update
更新フローは、次の順が基本です。
- metadata を取得
- 署名と有効期限と version を検証
- 成果物を staging 領域へダウンロード
- hash / size / 署名を検証
- 旧版を残したまま activation 準備
- 再起動時か専用 helper で切り替え
- 初回起動の健全性確認
- 問題があれば rollback
ここで重要なのは、 検証が終わる前に置き換えない 失敗したら進めない の 2 つです。
4.7 updater の権限を絞る
updater 全体を管理者権限で動かすのは避けたいです。
理想は次の分離です。
- ダウンロードと検証: 低権限
- 実ファイル置換だけ: 最小の権限を持つ helper
- helper は「検証済み package を所定場所へ置く」以上のことをしない
権限昇格が必要な設計ほど、昇格前に何を検証済みかをはっきり分けないと危なくなります。
4.8 rollback / freeze / mix-and-match を最初から潰す
ここは後から足すとつらいので、最初に入れたほうがいいです。
-
rollback 対策 クライアントは「今まで見た最高の metadata version / release version」を保持し、それより古いものを拒否する
-
freeze 対策 metadata に expiry を持たせ、古すぎる metadata を拒否する
-
mix-and-match 対策 metadata 同士の整合性を持たせる。少なくとも manifest 自体に対象 artifact の hash / size / version を固定する
加えて、特定 build を拒否する blocklist や minimum allowed version を signed metadata で配れると、事故時の封じ込めが速くなります。
TUF をそのまま採用しなくても、この 3 つの性質はかなり重要です。
4.9 最初はフル更新から始める
差分更新は帯域には効きますが、最初の実装としては複雑です。
- どの旧版からどの新版へ当てる差分か
- 差分適用前の前提 hash
- 差分適用後の最終 hash
- 途中失敗時の復旧
- 部分適用や古い差分の掃除
このへんが一気に増えます。 初期版では、署名済みフルパッケージを安全に入れ替える ところまでで十分です。
5. 最小安全構成
フル TUF ほど大げさにしないとしても、自前 updater の最小安全構成はだいたいこの形になります。
5.1 クライアントが持つもの
クライアントが持つのは、サーバーの応答を疑うための材料です。ここが空だと、更新してよいかどうかをサーバーの言い分だけで決めることになります。
- 信頼する root 公開鍵、または固定した証明書チェーン
- 現在動作中の version
- 過去に見た最高の metadata version / release version
- 許可する channel
- rollback 用の直前版
5.2 サーバーが返すもの
サーバーが返すのは、判断そのものではなく判断の材料です。どれも単体では信頼されず、5.1 の trust anchor を使った検証を通って初めて意味を持ちます。
- 署名された update metadata
- 署名済みまたは platform 署名された成果物
- 必要なら blocklist / minimum allowed version 情報
5.3 典型フロー
metadata を取る
↓
署名・expiry・version・channel を検証
↓
成果物を staging へ落とす
↓
size / hash / package signature を検証
↓
旧版を残したまま activation
↓
初回起動に失敗したら rollback
ここで大事なのは、更新サーバーの応答だけでは何も成立しない ことです。 成立させるのは、クライアントが持つ trust anchor と、検証ロジックです。
6. Windows 案件ではどう考えるか
Windows アプリでは、まず配布方式から逆算したほうが整理しやすいです。
- 要件が合うなら MSIX App Installer
- .NET の社内アプリで per-user が合うなら ClickOnce
- サービス、driver、shell extension、独自チャネル制御まで必要なら MSI + 独自 updater も比較対象
ただし、独自 updater を選んでも、やるべきことは減りません。 むしろ増えます。
- Authenticode / パッケージ署名の検証
- signed manifest
- rollback 対策
- 更新 helper の権限分離
- updater 自身の更新戦略
6.1 Authenticode 署名を、クライアントでどう検証するか
「Authenticode を検証する」と書くだけでは実装に落ちないので、Windows での入口を挙げておきます。
| やりたいこと | 手段 |
|---|---|
| updater のコードの中で検証する | WinVerifyTrust API(wintrust.dll)。WINTRUST_ACTION_GENERIC_VERIFY_V2 を指定すると、Authenticode の検証ポリシーが適用される |
| 運用手順や CI から確認する | PowerShell の Get-AuthenticodeSignature |
| リリース作業で署名と確認を行う | Windows SDK の signtool sign / signtool verify |
PowerShell なら、最小の確認はこれだけです。
$path = ".\MyApp-2.4.1-x64.msi"
$sig = Get-AuthenticodeSignature -FilePath $path
if ($sig.Status -ne 'Valid') {
throw "signature check failed: $($sig.Status) / $($sig.StatusMessage)"
}
# 「署名が有効」だけでは足りない。誰の署名かまで固定する。
# ただし Subject(識別名)は一意ではない。同じ CN/O/C を持つ証明書は
# 別の CA からいくらでも発行でき、その CA をこの端末が信頼していれば
# Status は Valid になり、Subject の比較も通ってしまう。
# 固定すべきは「発行元チェーン」または「公開鍵」で、Subject はその補助。
$expectedIssuers = @( # 発行元(中間CA/ルート)のサムプリント
'9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B' # ← 実際の値に置き換える
)
$expectedSubject = 'CN=Example Software Inc., O=Example Software Inc., C=JP'
# ここで組み直すチェーンは「発行元をたどる」ためだけのもので、
# 信頼できるかどうかの判定は上の Status = Valid で済んでいる。
# X509ChainPolicy の VerificationTime は既定でコンストラクターを呼んだ時刻
# (=現在)なので、そのまま Build すると、署名証明書が期限切れになった時点で
# NotTimeValid で失敗する。タイムスタンプ付きの署名は期限切れ後も Valid の
# ままなので、ここを既定にしておくと「正しく検証できた過去のリリースを、
# 証明書の更新をまたいだ瞬間に全部弾く」updater になる。
# 署名時刻が分かるならそれを VerificationTime に入れる。分からないなら、
# 期限の判定は上で済んでいるので、この Build では見ない
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
$chain.ChainPolicy.VerificationFlags = 'IgnoreNotTimeValid'
$chain.ChainPolicy.RevocationMode = 'NoCheck'
try {
$built = $chain.Build($sig.SignerCertificate)
# 署名者からルートまでのどこかに、期待する発行元が居ること。
# Build に失敗したときはチェーンが途中までしか埋まらないので、
# その場合もこの判定で落ちる(fail-closed)
$chainThumbprints = @($chain.ChainElements | ForEach-Object { $_.Certificate.Thumbprint })
if (-not ($expectedIssuers | Where-Object { $chainThumbprints -contains $_ })) {
throw "unexpected issuing chain (built=$built): $($chainThumbprints -join ' / ')"
}
}
finally {
$chain.Dispose()
}
if ($sig.SignerCertificate.Subject -ne $expectedSubject) {
throw "unexpected signer: $($sig.SignerCertificate.Subject)"
}
このとき、押さえておきたい制約が 5 つあります。
StatusがValidでも、それは「署名として正しい」という意味だけです。 誰の署名かは別に確認しますSubjectの一致は「本人であること」の証明になりません。 識別名は一意な識別子ではないからです。企業内 CA でも公開 CA でも、CN=Example Software Inc., O=Example Software Inc., C=JPという同じ識別名のコード署名証明書は発行できます。その CA をクライアントが信頼していれば、別の鍵・別の発行元で署名された差し替え版が、Status = ValidかつSubject一致で通ります。固定するのは発行元チェーン(期待する中間 CA / ルートのサムプリントが署名者からルートまでの経路に含まれること)か公開鍵にして、Subjectはその上での絞り込みに使います- 固定した以上、更新の手順を先に決めておきます。 証明書や CA を切り替える日に全端末の更新が止まる、という壊れ方をするからです。期待値は必ず「配列」で持ち、新旧を並べて受け付けられる状態を作ってから切り替えます(新しい発行元を許可リストに追加した updater を先に配る → 署名を切り替える → 古い値を落とす、の3段)。この「先に配る」ができるのは updater 自身を更新できるときだけなので、6 章冒頭に挙げた「updater 自身の更新戦略」と一体で設計してください
- timestamp が付いていない署名は、証明書の有効期限が切れた時点で検証を通らなくなります。 リリース時に必ず timestamp を付けます。4.5 で timestamp 付き署名を挙げているのは、この理由です
- 発行元をたどるための
X509Chainは、現在時刻で有効期間を再判定させないでください。X509ChainPolicy.VerificationTimeは既定でコンストラクターを呼んだ時刻、つまり現在です。ドキュメントも「署名されたメッセージを検証する場合、署名は検証時ではなく署名時に有効でなければならないので、このプロパティが重要になる」と明記しています。ここを既定のままにすると、timestamp が効いてStatus = Validになったパッケージを、そのすぐ後ろで「証明書が期限切れ」として弾くことになります。timestamp を付けた意味が消えるうえ、証明書を更新した瞬間に過去のリリースが全部落ちます。署名時刻が分かるならそれをVerificationTimeに入れ、分からないならIgnoreNotTimeValidを付けます。弱めているわけではありません。有効期間と信頼の判定は 1 つ上のStatus = Validで済んでいて、このBuildは「誰が発行したか」を調べるためだけのものだからです。同じ理由で失効確認もここでは行いません(期限切れの証明書の CRL は、そもそも発行され続けるとは限りません)。鍵が漏れたときに止める手段は、CRL ではなく blocklist と minimum allowed version(4.8)です
なお、検証結果はその端末の証明書ストアと信頼設定に依存します。クライアント側の信頼設定が緩い環境では、Status = Valid の意味も緩くなります。上のように updater 自身が期待する発行元を持っていれば、端末側の信頼設定が広がっても影響を受けません。
Windows でありがちな危ない形は、DownloadFile -> unzip -> kill process -> overwrite -> restart の一直線です。
これは動くことはありますが、セキュリティと復旧性の両方が弱いです。
SmartScreen や UAC の警告を「詳細情報 → 実行」で乗り切らせる運用は、更新設計ではなく警告馴化です。 正しい更新経路を作るなら、警告を慣れさせるのではなく、警告が出にくい配布と検証の構成に寄せるべきです。
配布方式の比較そのものは、次の記事でも整理しています。 Windowsアプリ配布方式の選び方 - MSI/MSIX/ClickOnce/xcopy/独自更新
7. 最低限のチェックリスト
自前 updater を出す前に、最低でもこのくらいは確認したいです。
- 更新 metadata は署名されている
- metadata には version / hash / size / channel / expiry が入っている
- クライアント側で署名と version を検証している
- 署名者の固定を、識別名(Subject)ではなく発行元チェーンか公開鍵で行っている
- 固定した値の切り替え手順(新旧を並べて受け付ける期間)が決まっている
- 成果物の hash と platform 署名を検証している
- 本番署名鍵は開発環境から分離されている
- 鍵の利用ログと承認記録が残る
- timestamp 付き署名を使っている
- staging 更新で、旧版を残したまま切り替える
- rollback の条件と手順がある
- 検証失敗時は fail-closed で止まる
- updater 自体の更新方針がある
- blocklist / minimum allowed version を配れる
- 段階配信を止める kill switch がある
- 失敗率、rollback 率、署名検証失敗を観測できる
このチェックリストに空きが多いなら、先に updater の UI を作るより、配布信頼モデルを詰めるほうが効果があります。
8. まとめ
自動アップデート機能のセキュリティで考えることは、結局これに尽きます。
更新の便利さではなく、 誰を信頼し、その信頼をクライアントがどう検証するか を設計する。
そのうえで、実務向けの判断をざっくり言うとこうです。
- 既存基盤で足りるなら、まずはそれに乗る
- 自前 updater を作るなら、HTTPS より先に署名付き metadata と鍵管理を入れる
- 失敗時の復旧と rollback を設計しない updater は、本番でつらい
- updater は配布機能ではなく、製品のセキュリティ境界そのもの
もし今の構成が latest.json + zip 差し替え に近いなら、最初に直すべきはダウンロード処理より信頼の置き方です。
ここを直すだけで、危険度はかなり変わります。
9. 参考資料
- CISA Secure by Design Pledge
- NIST: Security Considerations for Code Signing
- NIST Secure Software Development Framework (SSDF)
- The Update Framework Specification
- TUF: Roles and metadata
- TUF: Security
- Microsoft Learn: Authenticode Digital Signatures
- Microsoft Learn: WinVerifyTrust function
- Microsoft Learn: Get-AuthenticodeSignature
- Microsoft Learn: Auto-update and repair apps - MSIX
- Microsoft Learn: ClickOnce Deployment and Security
- Apple Developer: Developer ID
- CA/Browser Forum: Baseline Requirements for the Issuance and Management of Publicly-Trusted Code Signing Certificates
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
Windowsアプリ開発
自動更新は UI だけの話ではなく、配布方式、権限、復旧、運用まで含む設計です。Windows アプリの新規開発や既存ソフトの見直しで、更新方式の整理から対応できます。
技術相談・設計レビュー
「独自 updater が必要なのか」「MSIX / ClickOnce で十分なのか」「いまの更新設計のどこが危ないのか」といった整理段階から相談できます。
著者プロフィール
小村 豪
合同会社小村ソフト 代表
Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Windowsで「Windows によって PC が保護されました」が出る理由
Windowsアプリ配布時にSmartScreen警告が出る理由を、コード署名、EV/OV証明書、Azure Artifact Signing、MSIX、Microsoft Store、ClickOnce、社内配布、App Controlまで実務目線で整理します。
Windowsアプリ配布方式の選び方 - MSI/MSIX/ClickOnce/xcopy/独自更新
Windows アプリの配布方式はインストーラ形式の好みではなく、OS との結合度と更新責任の選択です。MSI / MSIX / ClickOnce / xcopy / 独自 updater を実務目線で整理します。
WinForms / WPFアプリのCI/CD実践 ── GitHub Actionsでビルドから署名・配布まで自動化する
WinForms / WPFアプリのCI/CDをGitHub Actionsで組む実務ガイド。windows-latestでのビルド+テストの最小YAML、タグ駆動のバージョン採番、signtoolによる署名の組み込み、MSI/MSIX/ClickOnce/xcopy別のC...
自社開発のWindowsアプリがウイルス扱いされたら ── Microsoft Defenderの誤検知対応と、性能影響との付き合い方
自社開発のWindowsアプリがMicrosoft Defenderに誤検知されたときの正規の対処を整理します。現代のウイルス対策の仕組み、Microsoftへの誤検知報告、隔離からの復元、除外設定の正しい入れ方とリスクまで解説します。
ClickOnce とは何か - 仕組み、更新、向いている場面・向いていない場面を実務目線で整理
.NET の Windows デスクトップアプリ配布で使われる ClickOnce について、マニフェスト、更新、キャッシュ、署名、向いている案件・向いていない案件を Mermaid 図つきで整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
Windows アプリの配布、更新、署名、rollback、MSIX / ClickOnce の選定は、実装だけでなく配布方式と運用設計まで含めて考える必要があるためです。
技術相談・設計レビュー
自動アップデートの信頼境界、signed metadata、鍵運用、fail-closed の設計は、個別実装よりも全体アーキテクチャの整理が重要になるためです。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- 自動アップデートはHTTPSで通信していれば安全ではないのですか?
- TLSは必要ですが十分条件ではありません。TLSで守れるのは主に通信経路と接続先の正当性であり、更新サーバーそのものが侵害された場合や、誤った成果物が正規CDNへ置かれた場合、未署名のmanifestが差し替えられた場合には対応できません。更新判定は「サーバーがそう言っているから」ではなく、「クライアントが署名付きmetadataを検証して正しいと判断できたから」にする必要があります。
- 更新用のmetadataには何を入れて署名すべきですか?
- 更新の判断材料を全部signed metadataに集約するのが基本です。具体的には、release version、成果物のURLとファイル名、hashとsize、channel(stable / betaなど)、対象OSやアーキテクチャ、minimum updater version、metadataの有効期限(expires_at)、必須更新かどうかのフラグなどです。バイナリだけ署名してmanifestを未署名のままにすると、URLやバージョンや必須更新フラグを改ざんされる余地が残ります。
- ロールバック攻撃とは何ですか?どう防ぎますか?
- 署名された正規版であっても、既知の脆弱性がある古い版を攻撃者が再配信し、脆弱版へ戻させる攻撃です。署名自体は正しいため、署名検証だけでは防げません。対策として、クライアントは今までに見た最高のrelease versionを保持し、それより古いものを拒否します。あわせて、metadataに有効期限を持たせて新版を見せないfreeze攻撃を防ぎ、manifestに成果物のhash・size・versionを固定してmix-and-match攻撃も潰しておくべきです。
- 自前のupdaterを作るべきですか?それとも既存の仕組みを使うべきですか?
- 要件が合うなら、まずMSIX App InstallerやClickOnceなど既存の更新基盤を優先するのが安全です。更新の責任範囲をプラットフォームへ寄せられるからです。自前updaterが必要になるのは、複数チャネルの厳密な制御や段階配信など、既存基盤に乗らない要件がある場合です。その場合も、最初に入れるべきはUIではなく署名検証と失敗時復旧で、検証に失敗したら進めないfail-closedを守り、旧版を残したまま切り替えてロールバックできるようにします。