BambooのCIのためにWindowsでコードサイニングするためのツールを作った

#はじめに
Windowsで動くコードサイニングするコマンドラインのアプリケーションを作りました。
製品開発とかじゃないので作りは適当ですが、部分的に参考になればと思います。

#機能の紹介
名前はDSignerです。コードは[GitHub](https://github.com/dmorita0108/DSigner)に公開しています。

##できること
- ストアした証明書を使った署名
- 単独のファイルか、指定したフォルダ内のファイルを全て署名する（再帰的）
- SHA1のみ/SHA2のみ/SHA1+SHA2デュアルサイン
- 署名済みのファイルをスキップする
- configで設定した拡張子のファイルに署名する
- configで設定した拡張子のファイルにカーネルモードドライバ用のクロスサインをする
- ホワイトリスト/ブラックリストを使った署名ファイルのフィルタリング（jsonファイルに正規表現を書いて実行時に渡す）

##できないこと
- PFXとパスワードを使った署名（configにあるけど使えるように作ってない）
- 署名済みのファイルに上書きサインする
- SignTool.exeで署名できないファイルに署名する（ps1とかxmlとか）
- タイムスタンプなしで署名
- ファイルのアトリビュートを見て署名するかどうか決める

##設定
DSigner.exeと同じ場所にあるconfig.jsonを設定しないと使えません。

```json:config.json
{
  "CertificateSettings": {
    "SHA1": {
      "StoreName": "ストア名",
      "SubjectName": "ストアした署名の名前",
      "Thumbprint": "ストアした署名のThumbprint",
      "TimestampURL": "タイムスタンプのURL",
      "CrossCertPath": "クロス署名用のファイルパス",
      "PfxPath": "ここは書いても何もおきません",
      "PfxKey": "ここは書いても何もおきません"
    },
    "SHA2": {
      "StoreName": "ストア名",
      "SubjectName": "ストアした署名の名前",
      "Thumbprint": "ストアした署名のThumbprint",
      "TimestampURL": "SHA2 TimeStampのURL",
      "CrossCertPath": "クロス署名用のファイルパス",
      "PfxPath": "ここは書いても何もおきません",
      "PfxKey": "ここは書いても何もおきません"
    }
  },
  "SigningSettings": {
    "SignToolPath": "SignTool.exeのパス",
    "SignExtensions": [
      ".exe",
      ".dll",
      ".sys",
      ".cab",
      "ここに拡張子を追加できます"
    ],
    "KernelModeExtensions": [
      ".sys",
      "ここにクロス署名する拡張子を追加できます"
    ],
    "TrialLimit": 3,
    "SigntoolProcessTimeoutMilliseconds": 10000 
  }
}
```

##使い方
コマンドラインのコマンドです

```
// 特定のファイルにSHA1でサイン
DSigner SHA1 -f ファイルパス

// 特定のフォルダ以下のファイル全てにSHA2でサイン（ホワイトリストでフィルタ）
DSigner SHA2 -d フォルダパス -w ホワイトリストのjsonファイルパス

// 特定のフォルダ以下のファイル全てにSHA1+SHA2デュアルサイン（ブラックリストでフィルタ）
DSigner Dual -d ファイルパス -b ブラックリストのjsonファイルパス
```

#参考になりそうなところ
既にファイルが署名されていないか確認するには、SignTool.exeでVerifyするとエラーが返ってくるかどうかでわかりますが、エラーが返ってくるとビルドが失敗扱いになってしまうことがあります。そのため確認にはWin32APIのWinVerifyTrustを使っています。

```c#:Signer.cs
private static bool IsTrustedFile(string filename)
{
    //WinVerifyTrust cannot work when "" exists in filename
    string file = filename;
    if (string.IsNullOrEmpty(file))
    {
        return false;
    }
    if (file.StartsWith("\""))
    {
        file = filename.Substring(1);
    }
    if (file.EndsWith("\""))
    {
        file = file.Substring(0, file.Length - 1);
    }

    NativeMethods.WinTrust.WINTRUST_FILE_INFO fileInfo = new NativeMethods.WinTrust.WINTRUST_FILE_INFO(file);
    NativeMethods.WinTrust.WinTrustData sWintrustData = new NativeMethods.WinTrust.WinTrustData(
        NativeMethods.WinTrust.WinTrustDataUIChoice.None,
        NativeMethods.WinTrust.WinTrustDataRevocationChecks.None,
        NativeMethods.WinTrust.WinTrustDataChoice.File,
        NativeMethods.WinTrust.WinTrustDataStateAction.Verify,
        0,
        NativeMethods.WinTrust.WinTrustDataUIContext.Execute,
        fileInfo
    );

    NativeMethods.WinTrust.WinTrustErrorCode ret = NativeMethods.WinTrust.WinVerifyTrust(
        IntPtr.Zero,
        NativeMethods.WinTrust.WINTRUST_ACTION_GENERIC_VERIFY_V2
        , sWintrustData);

    sWintrustData.Dispose();
    fileInfo.Dispose();

    if (ret != NativeMethods.WinTrust.WinTrustErrorCode.SUCCESS)
    {
        return false;
    }
    return true;
}
```

#おわりに
そもそもコードサイニングは自動化すべきでないとか色々意見があると思いますが、参考になれば幸いです。