🪪

Azure Key VaultでEVコードサイニング証明書を管理する

2025/02/22に公開

ソフトウェアの信頼性とセキュリティを確保するために、コードサイニング証明書を使用してプログラムにデジタル署名を行うことは重要です。特に、EV(Extended Validation:拡張検証)コードサイニング証明書は厳格な認証プロセスを経て、適切な法人のみに対して発行されるため、Windows SmartScreenなどのセキュリティ機能において高い信頼性を得ることができます。特に、アンチウイルス製品・EDR/XDR製品の警告や誤検知を抑制できることがあるという点は大きな利点といえます。これらは、ビジネスリスクとして認識すべき問題ですから、配布するバイナリの整合性やブランディング以上の価値がある場合も少なくないでしょう。

EVコードサイニング証明書

EVコードサイニング証明書はCA/Browser Forumの規定[1]により、FIDO[2]準拠のハードウェアセキュリティモジュール(HSM)に秘密鍵を保管することが義務付けられています。秘密鍵はこれらHSMデバイス上で生成され、生涯を通してこれらのデバイスから出ることはありません。この規約はCA(Certificate Authority:認証局)でも厳格な手続きのもと保証されており、特にDigiCertのオーダーでは、EVコードサイニング証明書をオーダーする際にオーダー担当者が法人を代理して、コードサイニング監査報告書という誓約書に合意して署名をする必要があります。

EVコードサイニング証明書は一部の信頼されたCAで販売されています。一般的には、ほとんどのケースでFIDO基準を満たす物理的なUSBデバイスが配送されるか、YubiKey 5 NFC FIPSなど自身の所有しているFIDO準拠デバイスを使用します。一方で、これらのUSBデバイスは当然ながら署名時に物理的なマシンに接続されている必要があります。そうすると、署名プロセスを既存のCI/CDパイプラインに統合する際、物理的なマシンが存在し、かつ適切な内部ネットワーキングを通してオンラインである必要があるなど、大変な手間と運用コストがかかることとなります。そこで、本記事ではAzure Key Vaultが提供するクラウドHSMを使用して、特定の環境に依存することなくEVコードサイニング証明書を管理する方法を紹介します。

  • Certera
  • Comodo
  • DigiCert
  • Sectigo

クラウドHSMによるプロビジョニングをサポートしていないCAもあるため、オーダー前に確認すべき事項です。また、DigiCertはDigiCert® KeyLockerというCAと統合されたソリューションがあるので、特にAzureに拘る理由がなければ、より良い選択肢となるかもしれません。

プロセス

クラウドHSMを利用する場合、オーダー時にCSR(Certificate Signing Request:証明書署名要求)を提出する必要があります。CSRはCAが署名を行うために必要な公開鍵を含みます。CSRを生成する際に、HSMデバイス上で秘密鍵が生成されます。この証明書及び関連付けられた秘密鍵は、まだ信頼されたCAによって署名されていない状態ですから、署名に使用することはできません。次に、このCSRをCAに提出し、CAはこれを検証して署名を行います。このとき、はじめてCSRに紐づけられた秘密鍵と証明書は信頼されたCAによって署名され、有効なコードサイニング署名を行うことができる状態となります。CAはこのとき、中間CAを含む署名CAの証明書を発行します。この証明書をAzure Key Vaultにマージすることで、Azureを通して実際にコードサイニングを行うことができる状態となります。

おおまかなプロセスは下記のとおりです。尚、証明書のオーダー及び組織認証の具体についてはCAによって異なる場合があるため、本記事では省略します。

  1. CSR発行
  2. 証明書のオーダー
  3. 組織認証
  4. クラウドHSMに証明書を統合

Azure Key Vaultインスタンスの作成

Azure Key Vaultインスタンスを作成します。記事執筆時点では、HSMを利用するためにはPremium SKUが必要となります。既存のインスタンスを利用したい場合は、既存のインスタンスのSKUがPremiumであるかを確認する必要があります。

https://learn.microsoft.com/en-us/powershell/module/az.keyvault/new-azkeyvault

New-AzKeyVault `
    -ResourceGroupName "my-resource-group" `
    -VaultName "my-evcs-vault" `
    -Location "East US" `
    -Sku Premium `
    -EnabledForDeployment $true `
    -EnabledForTemplateDeployment $true `
    -EnableSoftDelete

証明書の生成・CSR発行

証明書の発行のために事前にポリシーを生成します。ここでは、下記の要件を満たすPKCS#12証明書を生成します。

Extended(Enchanced)Key Usages (EKUs) 1.3.6.1.5.5.7.3.3, 1.3.6.1.4.1.311.10.3.13
X.509 Key Usage Flags DigitalSignature, KeyEncipherment
Reuse Key on Renewal No
Exportable Private Key No
Key Type RSA-HSM
Key Size 4096

1.3.6.1.5.5.7.3.3はコードサイニングを目的とした証明書であることを示します。1.3.6.1.4.1.311.10.3.13は署名の有効期間を証明書の有効期間に制限する[3]ために指定されるMicrosoft Authenticode特有のEKUです。これらはコードサイニング証明書のEKUとなります。但し、一般的にCSR上のEKUはCAによって上書きされることが多いため、厳密に指定する必要がない場合もあります。

-KeyNotExportableは少し混乱するネーミングですが、秘密鍵をHSM外にエクスポートできるかどうかを指定します。EVコードサイニング証明書では、秘密鍵はエクスポート不可である必要があるため、$true=エクスポート不可を指定します。

-ValidityInMonthsはオーダーする証明書の有効期限にあわせて指定します。

-CertificateTransparencyは、公開ログに証明書の発行情報を記録することで、証明書の透明性を確保し、悪意のある証明書が発行された場合に検出しやすくするオプションですが、今回は関係がないので$falseを指定します。

https://learn.microsoft.com/en-us/powershell/module/az.keyvault/new-azkeyvaultcertificatepolicy

$policy = New-AzKeyVaultCertificatePolicy `
    -SubjectName "CN=MyCompany LLC" `
    -SecretContentType "application/x-pkcs12" `
    -IssuerName "Unknown" `
    -ValidityInMonths 12 `
    -KeyType "RSA-HSM" `
    -KeySize 4096 `
    -ReuseKeyOnRenewal $false `
    -KeyNotExportable $true `
    -EnhancedKeyUsage @("1.3.6.1.5.5.7.3.1", "1.3.6.1.4.1.311.10.3.13") `
    -KeyUsage @("DigitalSignature", "KeyEncipherment") `
    -CertificateTransparency $false

生成したポリシーを使用して証明書を生成します。

https://learn.microsoft.com/en-us/powershell/module/az.keyvault/add-azkeyvaultcertificate

Add-AzKeyVaultCertificate `
    -VaultName "my-evcs-vault" `
    -Name "MyEVCodeSigningCert01" `
    -CertificatePolicy $policy

最後に、生成した証明書のCSRを取得します。このCSRは開始・終了ヘッダーを含まない生のBase64文字列なので、CSRヘッダとフッタを追加します。

https://learn.microsoft.com/en-us/powershell/module/az.keyvault/get-azkeyvaultcertificateoperation

$operation = Get-AzKeyVaultCertificateOperation -VaultName "my-evcs-vault" -Name "MyEVCodeSigningCert01"
$csr = $operation.CertificateSigningRequest

# PEM形式のヘッダーとフッターを追加
$pemCsr = "-----BEGIN CERTIFICATE REQUEST-----`r`n"
$pemCsr += (0..[Math]::Floor(($csr.Length-1)/64) | ForEach-Object { $csr.Substring($_*64, [Math]::Min(64, $csr.Length-$_*64)) }) -join "`r`n"
$pemCsr += "`r`n-----END CERTIFICATE REQUEST-----"

# ファイルに保存する場合
$pemCsr | Out-File "MyEVCodeSigningCert01.csr" -Encoding ASCII

下記のようなCSRができあがります。このCSRは例のため、実際のオーダーには使用できません。

-----BEGIN CERTIFICATE REQUEST-----
MIICpjCCAY4CAQAwFjEUMBIGA1UEAxMLY29udG9zby5jb20wggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQC73w3VRBOlgJ5Od1PjDh+2ytngNZp+ZP4fkuX8
K1Ti5LA6Ih7eWx1fgAN/iTb6l5K6LvAIJvsTNVePMNxfSdaEIJ70Inm45wVU4A/k
f+UxQWAYVMsBrLtDFWxnVhzf6n7RGYke6HLBj3j5ASb9g+olSs6eON25ibF0t+u6
JC+sIR0LmVGar9Q0eZys1rdfzJBIKq+laOM7z2pJijb5ANqve9i7rH5mnhQk4V8W
sRstOhYR9jgLqSSxokDoeaBClIOidSBYqVc1yNv4ASe1UWUCR7ZK6OQXiecNWSWP
mgWEyawu6AR9eb1YotCr2ScheMOCxlm3103luitxrd8A7kMjAgMBAAGgSzBJBgkq
hkiG9w0BCQ4xPDA6MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD
AQYIKwYBBQUHAwIwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAIHhsDJV3
7PKi8hor5eQf7+Tct1preIvSwqV0NF6Uo7O6YnC9Py7Wp7CHfKzuqeptUk2Tsu7B
5dHB+o9Ypeeqw8fWhTN0GFGRKO7WjZQlDqL+lRNcjlFSaP022oIP0kmvVhBcmZqR
QlALXccAaxEclFA/3y/aNj2gwWeKpH/pwAkZ39zMEzpQCaRfnQk7e3l4MV8cfeC2
HPYdRWkXxAeDcNPxBuVmKy49AzYvly+APNVDU3v66gxl3fIKrGRsKi2Cp/nO5rBx
G2h8t+0Za4l/HJ7ZWR9wKbd/xg7JhdZZFVBxMHYzw8KQ0ys13x8HY+PXU92Y7yD3
uC2Rcj+zbAf+Kg==
-----END CERTIFICATE REQUEST-----

クラウドHSMに証明書を統合

生成されたCSRでEVコードサイニング証明書をオーダーし、組織認証など必要な手続きを終えると、CAはCSRを署名して、中間CA及びCSRを署名したCAの証明書・署名された証明書が送付されます。これは、多くの場合.{crt,cer}の拡張子を持つX.509証明書です。このX.509証明書をHSMにマージして、HSM上で署名待ちとなっている証明書の署名を完了します。

https://learn.microsoft.com/en-us/powershell/module/az.keyvault/import-azkeyvaultcertificate

Import-AzKeyVaultCertificate `
    -VaultName "my-evcs-vault" `
    -Name "MyEVCodeSigningCert01" `
    -FilePath "path/to/signed.crt"

AzureSignToolによる署名

AzureSignToolはsigntool.exeのAzure版です。秘密鍵はUSBトークンではなく、遠隔のクラウドHSMにあるため、signtool.exeでは署名を行うことができません。AzureSignToolはAzureの資格情報と証明書名、Key Vault名をコマンドライン引数としてとり、これらの処理を自動で行い、実行可能ファイルやPowerShellスクリプトに署名することを可能にします。

https://github.com/vcsjones/AzureSignTool

まずはサービスプリンシパルを生成します。

https://learn.microsoft.com/en-us/powershell/module/az.resources/new-azadserviceprincipal

$sp = New-AzADServicePrincipal -DisplayName "my-codesigning-app"

サービスプリンシパルにKey Vault Readerのロールを付与します。

https://learn.microsoft.com/en-us/powershell/module/az.resources/new-azroleassignment

New-AzRoleAssignment `
    -ApplicationId $sp.AppId `
    -RoleDefinitionName "Key Vault Reader" `
    -Scope "/subscriptions/<subscription>/resourceGroups/<resource>/providers/Microsoft.KeyVault/vaults/my-evcs-vault"

さらに、サービスプリンシパルにAzure Key Vaultのアクセスポリシーを設定します。

https://learn.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultaccesspolicy

Set-AzKeyVaultAccessPolicy `
    -VaultName "my-evcs-vault" `
    -ServicePrincipalName $sp.AppId `
    -PermissionsToCertificates get,list `
    -PermissionsToKeys sign

AzureSignToolをインストールします。

  • NuGet:
    dotnet tool install --global AzureSignTool
    
  • WinGet:
    winget install vcsjones.azuresigntool
    

下記のコマンドで、カウンター署名及びダイジェストともにsha256を指定して署名を行います。

& AzureSignTool sign `
    -kvu "<vault URL>" `
    -kvi "<ClientID>" `
    -kvs "<ClientSecret>" `
    -kvc "MyEVCodeSigningCert01" `
    -kvt "<TenantID>" `
    -tr "http://timestamp.digicert.com" `
    -td "sha256" `
    -fd "sha256" `
    -ph `
    -v `
    "path/to/be-signed.exe"

Github Actionsに統合する

AzureSignToolを使用することで、簡単に既存のCI/CDパイプラインに統合することができます。例として下記のようなワークフローを定義します。各シークレットは適切な資格情報をそれぞれ設定する必要があります。

.github/workflows/release.yml
name: Release

on:
  workflow_dispatch:

jobs:
  sign:
    name: Sign
    runs-on: windows-latest

    steps:
      - name: install azure sign tool
        run: dotnet tool install --global AzureSignTool

      - name: sign package
        shell: pwsh
        run: |
          & AzureSignTool sign `
            -kvu "${{ secrets.AZ_CS_VAULT_URL }}" `
            -kvi "${{ secrets.AZ_CS_CLIENT_ID }}" `
            -kvs "${{ secrets.AZ_CS_CLIENT_SECRET }}" `
            -kvc "${{ secrets.AZ_CS_CERT_NAME }}" `
            -kvt "${{ secrets.AZ_CS_TENANT_ID }}" `
            -tr "http://timestamp.digicert.com" `
            -td "sha256" `
            -fd "sha256" `
            -ph `
            -v `
            "path/to/artifact.exe"
脚注
  1. https://cabforum.org/working-groups/code-signing/requirements ↩︎

  2. https://en.wikipedia.org/wiki/FIDO_Alliance ↩︎

  3. 信頼できるタイプスタンプサーバでカウンター署名を施している限り、一度署名されたバイナリは証明書の有効期限に依らず有効です。これは署名後にタイムスタンプサーバが終了した場合も同様です。タイムスタンプ付きの署名は、その時点で証明書が有効であったことを証明します。[4] ↩︎

  4. https://learn.microsoft.com/en-us/windows/win32/seccrypto/time-stamping-authenticode-signatures ↩︎

Discussion