🔏

GitHub Actions上のCI/CDパイプラインでWindowsアプリにコード署名するにはどういう構成を取ったら良いか調べた

に公開

概要

Windowsアプリをインストーラーなどで配布する際は、アプリのバイナリが改竄されていないこと及びバイナリの出自を証明するためにコード署名した状態で配布するのが一般的になっている。
そのような中で、GitHub ActionsのCI/CDパイプラインでWindowsアプリにコード署名する構成はどのような構成になるのかを調べた。

全体像

GitHub Actions上に構築するシステムの全体像は概ね以下のような図のようになる。

GitHub ActionsのCI/CDパイプラインにコード署名のプロセスを組み込む場合、コード署名用秘密鍵はGitHub Actions上のWindowsマシンからアクセスできるクラウド上のHSM(Hardware Security Module)に格納しておく必要がある。[1]
クラウド上のHSMは、証明書ベンダー(CA)が用意しているものを使うパターンと、自分たちのクラウド上のものを使うパターンの2種類がある。前者の場合、年間のコード署名数に応じて課金されることがあり、またその費用が割と高い場合が多い。なので、自前運用が面倒でなければ自分たちのクラウドを繋げて使う方式がオススメ。

コード署名をする際にはタイムスタンプ付与することが一般的。タイムスタンプがない場合、コード署名を実施した日時は署名実施者の自己申告の値になるため、第三者は署名実施時点で証明書が有効だったかどうかを検証できない(署名実施するマシンの日時を過去日時にして署名するということができてしまう)。そのため、証明書の有効期限が切れると、コード署名も無効と見なされる。
これに対し、信頼できる第三者のタイムスタンプが付与されていると署名実施日時が証明可能となり、署名実施時点で証明書が確かに失効していなかったことを第三者が検証できるようになる。これにより、証明書の有効期限が切れた後もコード署名自体の有効性は維持される。

CAが発行したコード署名用証明書には秘密鍵などの機微なデータは含まれない。従って、GitHub Actions Runner上にファイル等の形で置くことができる。

コード署名用証明書を発行してもらうおおよその手順

まずは、何はなくともコード署名証明書を発行してもらう必要がある。
この手順を実施する際のポイントは次の通り。

  • コード署名用の秘密鍵はFIPS 140-2 level 2準拠の安全なHSM上で生成し、HSM内で保管する(秘密鍵はHSMの外部に取り出せないようにする)。
    • これは秘密鍵はそのように取り扱うこと、という業界要件があるため。この要件を満たさないとCAはコード署名証明書を発行してくれない。[1:1]
    • 要件的にはFIPS 140-2 level 2準拠のUSBセキュリティートークンに秘密鍵を保存することもできるが、GitHub Actions RunnerはUSBにアクセスできないので今回の構成では使えない。
  • CAのコード署名証明書は、秘密鍵に対応する公開鍵に対して発行される。
    • これはつまり『この公開鍵は確かに組織xxxのものです』という証明をCAにしてもらうと言うことに相当する。
  • 秘密鍵は一度生成したら、その安全性が損なわれない限り再作成は不要。なので、次回の証明書更新時に再作成する必要はなくそのまま使い続けられる。
  • 証明書は有効期限があるので、有効期限到来の都度発行が必要。

自社利用クラウドのHSMで秘密鍵を生成し証明書を発行してもらう場合は、大まかに次のような手順を取ることになる。

  1. コード署名用の秘密鍵をFIPS 140-2 level 2準拠の安全なHSM上で生成し、HSM内で保管する
    • ここで生成した秘密鍵は、鍵の安全性が損なわれていない限り再生成は不要。つまり、次年度以降にコード署名証明書を再び発行する際は次の手順2から行えば良い。
  2. 秘密鍵のAttestationを取得する
    • 秘密鍵のAttestationとは、秘密鍵が信頼できる環境で生成され改竄されておらず安全であることを第三者(この場合はCA)が確認できるようにするためのデータのこと。
    • AttestationはHSMからダウンロードすることになる。
  3. 秘密鍵を利用してCSR(Certificate Signing Request)を作成する
    • CSRには公開鍵と秘密鍵を使った署名が入っているので、CAが発行する証明書と鍵ペアはこのCSRを通じて紐付くことになる。
  4. CSRとAttestationの両方をCAに送信し、コード署名証明書を発行してもらう
    • CA側ではAttestationを元に秘密鍵の健全性を検証した上で、CSRに基づいたコード署名証明書を発行する
  5. CAから証明書ファイルを受領し、どこかに保存しておく

なお、手順3のCSR作成は手元にLinuxマシンを用意して次のようなOpenSSLコマンドで実施することが多い。
OpenSSLからクラウドのHSMを使う方法については OpenSSLとクラウドHSMやYubiKeyを連携可能にするOpenSSL Engine APIとは にまとめてあるので参照されたい。

export PKCS11_MODULE_PATH=/tmp/libkmsp11-1.6-linux-amd64-fips/libkmsp11.so 
export KMS_PKCS11_CONFIG=/tmp/pkcs11-config.yaml

openssl req -new -subj '/CN=example.com/' -sha256 \
  -key pub.pem -engine pkcs11 -keyform engine \
  -key pkcs11:object=sign_key_name > cert-request.csr

上述の秘密鍵の生成やCSR作成が面倒だという場合は、CAが用意しているHSMを利用するとこの辺りの手順が半自動で行える場合がある。

コード署名時の手順

証明書が発行されたら、GitHub Actions Runner上のWindowsマシンでコード署名をできる環境を整えることになる。
環境を整える際のポイントは次の通り。

  • コード署名はWindows SDK付属のSignTool.exeで行う(GitHub-hostedのWindowsマシンにはインストール済)[2]
  • コード署名処理をするSignTool.exeの署名処理をクラウド上のHSMに委譲できるようにするためのライブラリをGitHub Actons Windows Runnerにインストールする必要がある。
  • インストールしただけだとどの資格情報でクラウドにアクセスしどの鍵を使うのかが設定されていないので、各ライブラリの設定手順に従って設定する
    • 例えば、Google Cloud CNG Providerの場合は、Google Cloudにアクセスする際の資格情報はADCを利用し、どの鍵を利用するかはYAMLで定義することになる。
    • Digicertやssl.comなどのプロプライエタリなHSM環境と繋ぐ場合は、それぞれの会社が公開しているドキュメントを参考に繋ぐことになる。

上記環境が整ったら、後はSignTool.exeを呼び出しコード署名をするだけ。
以下の例は、Sectigoの証明書で署名するコマンドの例。

signtool.exe sign /v /fd sha256 /t http://timestamp.sectigo.com /f path/to/mysigncscertificate.crt /csp "Google Cloud KMS Provider" /kc projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY_NAME/cryptoKeyVersions/1 path/to/file-tobe-signed.exe
脚注
  1. 2023年以前は秘密鍵をPEMファイルに保存しておくことができた。しかし、NVIDIAのコード署名用秘密鍵流出に代表されるような秘密鍵流出インシデントが無くならなかったため、コード署名証明書を発行する際は秘密鍵がHSM上に保存されていることが業界要件になったためである。 ↩︎ ↩︎

  2. https://github.com/actions/runner-imagesでRunnerにインストールされているソフトウェアの一覧が見られる。 ↩︎

Discussion