ユーザ用ツール

サイト用ツール


azure_sign_tool

コード署名証明書をUSBトークンからクラウドHSMへ移行した

2026-07-07

要約: Windowsアプリケーションのコード署名において、USBトークンに格納されたコード署名証明書をAzure Key Vault Premium HSM上の証明書に移行し、GitHub Actionsでの自動署名を可能にした事例。秘密鍵の管理方式を「物理トークンの所持」から「Azure AD認証+RBACによるアクセス制御」に変更することで、CI/CDパイプラインへの組み込みと監査性の向上を実現した。

下記の説明は、一部の事実を改変している。

背景

従来、コード署名証明書はSafeNet eToken(USBトークン)に格納し、物理マシンにトークンを挿した状態で``signtool.exe``を用いて署名を行っていた。この構成には以下の課題があった。

  • USBトークンが挿さった物理マシンへの依存(RDP経由では利用不可)
  • GitHub ActionsなどのCIサービスでの自動署名が不可能
  • 複数アプリケーションのリリースパイプラインが単一マシンに集中
  • 署名操作の監査ログが取得できない

既存のコード署名証明書の有効期限が迫っていたため、このタイミングで認証局(CA)を乗り換え、クラウドHSM構成へ移行することとした。

移行先の選定

Azure Key VaultのHSMバックエンドに秘密鍵を格納し、Bring Your Own Key(BYOK)方式でコード署名証明書を発行できるCAを調査した。

CA Azure Key Vault BYOK対応 結果
従来のCA(A社) 非対応 見送り
D社 例外手続きが必要 見送り
GMOグローバルサイン 標準フローで対応 採用

GMOグローバルサインのOVコードサイニング証明書HSMを採用した。同CAはAzure Key Vaultへの証明書マージ手順を公式マニュアルとして提供しており、標準フローでBYOK構成を構築できる点が決め手となった。

移行の流れ

移行は5つのフェーズに分けて実施した。

フェーズ1: 事前確認

  • D社への問い合わせ(BYOKは例外手続きであることを確認)
  • GMOグローバルサインへの問い合わせ(BYOKが標準フローであることを確認)
  • 価格確認

フェーズ2: Azure Key Vaultの準備

  • リソースグループを``japaneast``に作成
  • Key VaultをPremiumプランで作成(RBAC有効)
  • HSMキー(RSA 4096, RSA-HSM)を生成
  • Key Vault上でCSRを生成・ダウンロード
  • CSRのSubjectは``CN=EXAMPLE INC., O=EXAMPLE INC., S=Tokyo, C=JP``に設定

フェーズ3: CA申し込み〜ローカル署名

  • GMOグローバルサインに申し込み
  • CSR提出、DUNS照合、電話確認、HSM鍵管理監査宣誓書を提出
  • 証明書発行後、Azure Portalで署名済み要求をマージ
  • サービスプリンシパルを作成し、RBACロール(Key Vault Crypto User + Certificate User)を付与
  • AzureSignToolを用いてローカル署名テストを実施 → ``Status: Valid``を確認

フェーズ4: GitHub Actionsでの自動署名

  • リハーサル用privateリポジトリを作成
  • 5つのRepository Secretsを登録(Key Vault URI, 証明書名, クライアントID, クライアントシークレット, テナントID)
  • GitHub Actionsワークフローを作成し、dummy実行ファイルのビルド→署名→検証→artifactアップロード→draft release作成を自動化
  • 署名済みバイナリをダウンロードし、ローカルで``Get-AuthenticodeSignature``による検証を実施 → ``Valid``確認済み

フェーズ5: 旧環境の整理

未着手。旧証明書の有効期限前に、本番リポジトリでの署名が安定した段階で実施予定。

新旧構成の比較

項目 旧構成 新構成
秘密鍵の所在 USBトークン内 Azure Key Vault HSM内
鍵のエクスポート 不可 不可(RSA-HSM, non-exportable)
署名に必要なもの 物理トークン + PIN Azure認証 + ネットワーク到達 + RBAC許可
署名ツール signtool.exe AzureSignTool
CI自動署名 不可 可能(GitHub Actions対応)
監査ログ なし Key Vault診断ログで取得可能
証明書の指定 ストアから自動選択(``/a``) 証明書名で明示指定(``-kvc``)

セキュリティモデルの変化

守るべき対象の変化

旧構成ではUSBトークン本体の物理的保護が最重要であった。新構成ではサービスプリンシパルのclient secretが最大の保護対象となる。client secretが漏洩すると、Key Vaultに到達可能な任意の環境から署名が行えるため、eTokenの盗難と同等の事故クラスとして扱う必要がある。

多層防御の構成

新構成では以下の多層防御を採用している。

  • HSM — 秘密鍵はハードウェアセキュリティモジュール内に保持され、エクスポート不可
  • RBAC — 最小権限の原則に基づき、署名専用SPにのみCrypto User + Certificate Userロールを付与
  • Azure AD認証 — クライアントID + クライアントシークレットによる認証
  • シークレット管理 — パスワード管理ツールとGitHub Secretsのみにclient secretを保管
  • 監査ログ — Key Vault診断ログで署名操作を追跡可能

コスト

項目 金額
GMOグローバルサイン OVコードサイニング証明書 HSM 60,000円/年(税抜)
Azure Key Vault Premium 月額100〜200円程度
AzureSignTool 無料(オープンソース)

所感

USBトークンによるコード署名は「物理トークンを挿す」という行為がそのままセキュリティの根拠となっており、理解しやすい反面、CI/CDとの親和性が著しく低いという欠点があった。クラウドHSMへの移行により、署名基盤をAPI化し、ビルドパイプラインの一部として扱えるようになったことは大きな前進である。

一方で、「USBを挿さなければ絶対に署名できない」という物理的制約がなくなることは、セキュリティモデルの変化を意味する。Azure ADの認証情報(特にclient secret)の管理が新たなクリティカルパスとなるため、OIDC(Workload Identity Federation)への移行やシークレットの定期ローテーションなど、継続的な運用改善が求められる。

また、GMOグローバルサインのHSM対応が標準フローであることは、Azure Key Vaultを署名基盤として採用する上で大きなアドバンテージであった。CAの選定段階でこの点を確認しておいて正解だったと言える。

署名スクリプト

リハーサル用リポジトリには、ローカル環境でCIと同一の署名手順を再現するPowerShellスクリプト(scripts/sign-dummy-local.ps1)を同梱した。このスクリプトは1Password CLI(op)と連携しており、client secretを環境変数やファイルに直接書かずにop run経由で注入できる。scripts/.env.op.exampleにAZURE_CLIENT_SECRETをop://形式で記述し、

op run --env-file=scripts/.env.op -- pwsh ./scripts/sign-dummy-local.ps1

のように実行する。

スクリプト内部で実際に呼び出しているAzureSignToolのコマンドは以下のとおりである。

azuresigntool sign `
    -kvu $KeyVaultUri `
    -kvc $CertName `
    -kvi $ClientId `
    -kvs $clientSecret `
    -kvt $TenantId `
    -tr http://timestamp.digicert.com `
    -fd sha256 -v `
    $exe

-kvuにKey VaultのURL、-kvcに証明書名、-kvi / -kvs / -kvtにサービスプリンシパルのクライアントID・シークレット・テナントIDを渡す。-trでタイムスタンプAuthorityを指定する点はsigntool.exeの/trと同じである。

GitHub Actionsのワークフロー(.github/workflows/sign-rehearsal.yml)では、このスクリプトは使わずにazuresigntoolを直接呼び出している。シークレットはRepository Secretsから環境変数経由で注入する。

- name: Sign executable
  env:
    AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
    CERT_NAME: ${{ secrets.CERT_NAME }}
    AZURE_CLIENT_ID: ${{ secrets.CLIENT_ID }}
    AZURE_CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}
    AZURE_TENANT_ID: ${{ secrets.TENANT_ID }}
  shell: pwsh
  run: |
    $exe = "dummy/out/DummyBin.exe"
    azuresigntool sign `
      -kvu $env:AZURE_KEY_VAULT_URI `
      -kvc $env:CERT_NAME `
      -kvi $env:AZURE_CLIENT_ID `
      -kvs $env:AZURE_CLIENT_SECRET `
      -kvt $env:AZURE_TENANT_ID `
      -tr http://timestamp.digicert.com `
      -fd sha256 -v `
      $exe

AZURE_CLIENT_SECRET環境変数が直接設定されている場合はそちらを優先するため、ローカルスクリプトもGitHub Actionsのシークレット注入(env:)と同じ仕組みで動作する。これにより、client secretがディスクに平文で残ることを防ぎつつ、ローカルでの署名テストとCIでの自動署名で同一の手順を共有できる。

参考リンク

azure_sign_tool.txt · 最終更新: by Takuya Nishimoto

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki