Electron ForgeでビルドしたAppXファイルにハードウェアトークンによるコード署名を行う方法

はじめに

本記事は、OPTiM TECH BLOG Advent Calendar 2025のDay 13の記事です。

こんにちは。技術統括本部の遠藤です。普段はSaaS 管理プロダクトのOPTiMサスマネのフロントエンドとエージェントアプリケーションの開発を担当しております。

この記事では、Electron ForgeでビルドしたAppX形式のファイルに対して、ハードウェアトークンによるコード署名を行う方法を紹介します。

要約

  • Electron Forgeはビルド時に「.pfx」形式のファイルによる署名を実行しようとする
  • ハードウェアトークンによる署名を実行したければ、postMakeフックで署名の上書きを実行する必要がある
  • 署名のスクリプトは自分で実装しなければならない

Electron Forgeとは

Electron Forgeは、デスクトップアプリ開発のためのフレームワークである「Electron」の、アプリ作成・ビルド・パッケージング・配布までまとめて面倒を見てくれるオールインワンツールです。 プロジェクトのひな型生成から、開発用コマンド、パッケージ作成、インストーラー生成、コード署名、リリース配布までを一貫して扱えます。

Electronでのアプリの開発は、JavaScript + HTML + CSSの技術を用いて行うことができます。

アプリケーションのコード署名(コードサイニング)とは

製作したアプリケーションを第三者の端末にインストールさせる場合、開発者の正当性とアプリケーションコードの改ざんがないことを証明する必要があります。この証明は、「コード署名(コードサイニング)」という技術によって実現されます。 コード署名されていないアプリケーションをインストールしようとすると、大抵はOSによってインストール処理がセキュリティ対策のためブロックされます。(以下のような画面が表示されます)

したがって、不特定多数に向けたアプリケーション開発をする際は、アプリケーションへのコード署名の対応が必須になります。

そして、コード署名に必要な秘密鍵を安全に保管し、署名処理を内部で実行する物理デバイスをハードウェアトークンと呼びます。

ハードウェアトークンには以下のような種類があります。

  • USBトークン: USB接続型のセキュリティデバイス.

  • HSM (Hardware Security Module): 大規模環境向けのネットワーク接続型デバイス.

本記事では、USB型のハードウェアトークンを前提として説明します。

また、HSMに関する説明は、過去のTech Blog(物理トークンからの脱却 AWSCloudHSM入門)で記載していますので、そちらを参照してください!

tech-blog.optim.co.jp

Electron Forgeでコード署名を行う

さて、先述の通りElectron Forgeでは、コード署名機能がサポートされています。 そのため、Electron Forgeでアプリケーション開発を行うユーザーは、コード署名用のスクリプトを自前で用意する必要もなければ、コード署名用にライブラリを導入する必要はありません。

これが大きな落とし穴でした。

公式リファレンスでの説明

公式リファレンスでは、AppX形式のファイルにコード署名を行う場合、forge.config.ts ファイルに以下の設定を追加すれば良いとされています。

module.exports = {
  makers: [
    {
      name: '@electron-forge/maker-appx',
      config: {
        publisher: 'CN={アプリケーション開発者のpublisher name}',
        devCert: '{コード署名に利用する.pfx形式の証明書ファイルのパス}',
        certPass: '{コード署名用に使うファイルのパスワード}'
      }
    }
  ]
};

この設定がある状態で、 pnpm run electron-forge makeコマンドを実行することで、自動的にコード署名されたAppX形式のファイルが出力されます。

しかし、ここで問題があります。ハードウェアトークンによる署名では、.pfx形式の証明書ファイルを使用しません。

落とし穴

実は、Electron ForgeではAppX形式のファイルに対するハードウェアトークンによる署名がサポートされていません。 リファレンスを読めばわかるのですが、.pfx形式のファイルによる署名を前提としています。 また、devCertオプションで無効なファイルパスを指定する、もしくはこのオプションの設定自体を行わなかった場合、Electron Forgeはpfx形式の証明書ファイルを生成した後、生成したファイルを使用してコード署名を実行しようとします。

よって、AppXのファイルをビルドする時点で、ビルド成果物に対するpfx形式の署名書ファイルによるコード署名は絶対に行われてしまいます。これは、Electron Forgeの実装(MakerAppX.ts)を読んで確認することもできます。

// NOTE: AppXファイルのmake実行時にコールされる関数
 async make({
    dir,
    makeDir,
    appName,
    packageJSON,
    targetArch,
  }: MakerOptions): Promise<string[]> {
    const outPath = path.resolve(makeDir, `appx/${targetArch}`);

   // 省略

   // NOTE: devCertのオプションが存在しない場合、pfx形式のデフォルトの証明書を作成する
    if (!opts.devCert) {
      opts.devCert = await createDefaultCertificate(opts.publisher, {
        certFilePath: outPath,
        program: opts,
      });
    }
   // 省略
}

ハードウェアトークンによる署名を行う必要がある場合、これでは非常に困ったことになります。

ハードウェアトークンによる署名を行う方法

AppXファイルへのハードウェアトークン署名は、以下の二段階で実現します

  1. Electron Forgeによる.pfx証明書での署名
  2. ハードウェアトークンによる署名

これは、Electron Forgeが提供する「フック」を使うことで実現できます。 Electron Forgeでは、ビルドプロセス中の特定のタイミングでユーザーが定義した割り込み処理を実行できる「フック」と呼ばれる仕組みがあります。(参考: https://www.electronforge.io/config/hooks )

今回は、AppXファイルに対する.pfx証明書での署名後にハードウェアトークンによる署名の処理を実行させるために、postMakeフックで署名を実行する処理を記述します。

postMakeで実行するsigntoolの署名が最終的な署名として上書きされるため、ハードウェアトークンによる署名を成果物に残せます。 postMakeフックで署名処理を実装する場合のサンプルコードは以下の通りです。

module.exports = {
  hooks: {
    // appxファイルに対して署名を実行するサンプルコード
    postMake: async (_forgeConfig, makeResults) => {
        const timestampServer = 'http://timestamp.digicert.com';
  
        for (const result of makeResults) {
            for (const artifact of result.artifacts) {
                if (artifact.endsWith('.appx')) {
                    // NOTE: {{thumbprint}}の値は、ハードウェアトークンのthumbprint(拇印)を指定する
                    const command = `signtool sign /sha1 {{thumbprint}} /fd sha256 /td sha256 /tr \$${timestampServer} "$${artifact}"`;
                    execSync(command, { stdio: 'inherit' });
                }
            }
        }
    }
  }
};

コード署名の処理自体はsigntoolコマンドを呼び出す形で実行します。signtoolコマンドは、Electron forgeが.pfx証明書で署名を実行する際に呼び出されるコマンドでもあります。 サンプリコード内の{{thumbprint}}には署名に利用するハードウェアトークンのthumbprint(拇印)の値を指定してください。また、署名対象の.appxファイルパスは、postMakeフックのmakeResultsから取得することができます。makeResultsは以下の値を持つオブジェクトです。

type MakeResults {
    arch: string;
    artifacts: string[]; // make実行後に出力されたファイル群のファイルパスが格納されている
    packageJSON: any;
    platform: string;
}

サンプルコードでは、makeResults.artifacts[]の中から、.appxファイルのみを抽出し、それらのファイルに対して署名を実行する処理を記載しています。

あとは、署名を実行する端末にハードウェアトークンを挿し込みログインした状態で、pnpm run electron-forge makeコマンドを実行するとハードウェアトークンによる署名処理が自動的に実行され、コード署名された.appxファイルを得ることができます。

また、サンプルコードには記載していませんが、展開される.exeファイルなどのバイナリファイルに対しても署名を実行したい場合は、パッケージングが終わった段階、すなわちpostPackageフックでサンプルコードと同様に、ビルド成果物のファイル群の中から、.exeファイルを抽出し、それらに対して署名を実行する処理を書けばOKです。

まとめ

今回は、Electron Forgeで生成されるファイルに対してハードウェアトークンによるコード署名を実行する方法を紹介しました。

AppXファイルへのコード署名処理の実装にあたり、Electron Forgeに関する情報がインターネット上にあまり存在しないため、情報収集に非常に苦労しました。公式リファレンスなどにも十分な情報がない場合は、リポジトリにアクセスして実装コードを直接読んでしまった方が早いです。(実際、Electron Forgeコード署名がどのような処理を経て実行されているかはコードを読むことで理解できました) 技術的にチャレンジングな課題で、自身のエンジニアリングスキルの大きな成長に繋げることができたと思います。

オプティムでは様々な技術的な課題に一緒に取り組めるエンジニアを幅広く募集しています!興味があれば、ぜひご応募ください!

www.optim.co.jp