2025-11-04 2025-11-11

概要

ビルドしたwindows用のexeにデジタル署名(コードサイニング証明書)を付与する手順を記載する。

目次

デジタル署名とは

デジタル署名とは Windows の SmartScreen やセキュリティ警告を回避するために、
実行ファイルが信頼できる開発者によって発行されたものである事を証明する為の仕組みで、
以下の手順により、exe に署名を行う。

  • 認証局(CA)から コードサイニング証明書(Code Signing Certificate) を購入する
  • exe に署名ツールで証明書を付与する

認証局

コードサイニング証明書の種類と費用

種類証明書発行先説明年額目安(USD)SmartScreen 対応
Standard Code Signing個人または法人最も一般的$70〜$200×(警告は残る)
EV Code Signing法人のみ(住所・電話番号確認あり)USBトークン発行。SmartScreen即信頼$300〜$600

主な認証局(2025年現在)

発行元StandardEV備考
DigiCert約 $400約 $600信頼性高い
Sectigo (旧 Comodo)約 $80〜$100約 $300最もコスパ良い
GlobalSign約 $250約 $500法人向け多い
Certum (ポーランド)約 $60約 $200個人開発者に人気(安価)

※金額は年額

個人開発者が使う場合

個人でも利用出来て、比較的安価なのは Certum の Standard Code Signing ぐらい?
https://shop.certum.eu/code-signing.html

EV証明書であれば SmartScreen 警告は出なくなるが、EV証明書は法人用の為、個人では利用できない。
なので、SmartScreen の「発行元不明」警告を減らすには、複数回署名してユーザーに配布する事で信頼を蓄積するしかない。

証明書のタイムスタンプについて

コードサイニング証明書には有効期限があるが、
有効期限内に署名し、署名時に「タイムスタンプサーバー」から日時を刻印しておけば、
「この署名は証明書が有効だった時点で行われた」と判断され、期限切れ後も信頼される。

主なタイムスタンプサーバ

認証局 (CA)URL備考
DigiCerthttp://timestamp.digicert.com最も安定・推奨。HTTP/HTTPS両対応。
GlobalSignhttp://timestamp.globalsign.com/scripts/timestamp.dll旧形式のURLだが動作安定。
Certumhttp://time.certum.pl無料利用可能。個人開発者にも人気。
VeriSignhttp://timestamp.verisign.com/scripts/timstamp.dll歴史的に古いが互換性あり。
SSL.comhttp://timestamp.ssl.com新しめだが信頼性あり。

自己署名

前述の通り、個人開発者であれば上記の認証局を使用しても、即時に SmartScreen警告は回避出来ない。
※複数回署名してユーザーに配布する事で信頼を蓄積するしかない。

そこで、ここでは自分でCAを立てて署名する手順を記載する。(結局はすぐに警告は回避できないので)
自己署名の場合、自己CAを信頼済みとして登録しても SmartScreen 警告は残るが、
プログラムから「起動しようとしているexeが自分で作成したものか」というチェックを行う事は出来る。
※自己署名の場合、プログラムによる署名検証等は行う事が出来るが、Windows環境下での警告回避は出来ない点には留意する。

自己証明書作成

以前に Javaでhttps通信時の証明書検証について で作成した証明書作成用のシェルを今回用に少し弄って使用する。
※きちんと自己CAを立てる場合は、サーバ名がIPアドレスの場合のSSL証明書作成 を参照。

make_cert.sh

#!/bin/bash

rm -rf *.key
rm -rf *.csr
rm -rf *.crt

cat <<_MY_CONF_ > mycert.cnf
[ req ]
default_bits = 2048
default_md = sha256
prompt = no
encrypt_key = no
distinguished_name = dn

[ dn ]
C = JP
O = My Company
CN = My Code Signing

_MY_CONF_

# 証明書要求の作成
openssl req -new -config mycert.cnf -keyout myapp.key -out myapp.csr

# 署名
openssl x509 -days 365 -req -signkey myapp.key -extfile mycert.cnf < myapp.csr > myapp.crt

# PFX形式に変換
openssl pkcs12 -passin pass:"" -passout pass:"" -export -out myapp.pfx -inkey myapp.key -in myapp.crt

# 作成した証明書の内容を表示
openssl x509 -noout -text -in myapp.pfx

exeへの署名

Windows環境でWindows用に署名

Windowsの場合、signtool で署名する。

Mac/Linux環境でWindows用に署名

  • osslsigncodeのインストール
     
    brew install osslsigncode
    
    ※Linux の場合は apt 等でインストール
     
  • osslsigncode で署名
     
    osslsigncode sign \
      -pkcs12 myapp.pfx \
      -pass mypassword \
      -n "MyApp" \
      -i "https://example.com" \
      -in myapp.exe \
      -out myapp-signed.exe \
      -t http://timestamp.digicert.com
    
     
  • 署名検証
     
    osslsigncode verify myapp-signed.exe
    

プログラムによる署名検証

pythonによる署名検証(手っ取り早くPowerShellを使用する)

import subprocess
import json

def get_signature_info(exe_path: str):
    ps_script = f"""
    $sig = Get-AuthenticodeSignature '{exe_path}';
    $obj = [PSCustomObject]@{{
        Status = $sig.Status.ToString();
        Signer = $sig.SignerCertificate.Subject;
        Issuer = $sig.SignerCertificate.Issuer;
        Thumbprint = $sig.SignerCertificate.Thumbprint;
    }};
    $obj | ConvertTo-Json -Compress
    """
    result = subprocess.run(["powershell", "-Command", ps_script], capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError("署名情報の取得に失敗しました")

    info = json.loads(result.stdout)
    return info


if __name__ == "__main__":
    exe = "dist/app.exe"
    info = get_signature_info(exe)
    print("署名検証結果:")
    print(json.dumps(info, indent=2, ensure_ascii=False))

    # 署名者(Signer)、証明書ハッシュ(Thumbprint) などを照合
    if "My Company" in info["Signer"]:
        print("OK")
    else:
        print("NG")

(結果例)

署名検証結果:
{
  "Status": "UnknownError",
  "Signer": "O=My Company, L=xxxx, S=xxxx, C=JP, CN=xxx.xxx.xxx",
  "Issuer": "CN=xxx.xxx.xxx, O=xxxx, L=xxxx, S=xxxx, C=JP",
  "Thumbprint": "81CD829A58A13480C...."
}
OK

補足

PowerShellを使用せずに署名を参照/チェックする場合のサンプルコード。

import pefile
import sys
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.serialization import pkcs7


def get_exe_thumbprint(path: str) -> str:
    pe = pefile.PE(path)
    dir_entry = pe.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY']]
    if dir_entry.VirtualAddress == 0:
        raise ValueError("署名が存在しません。")

    data = bytes(pe.write())
    offset = dir_entry.VirtualAddress
    end = offset + dir_entry.Size

    thumbprint = None
    while offset < end:
        # 証明書格納位置を判定し情報取得
        # https://learn.microsoft.com/windows/win32/debug/pe-format#the-attribute-certificate-table-image-only
        dw_length = int.from_bytes(data[offset:offset + 4], "little")
        cert_type = int.from_bytes(data[offset + 6:offset + 8], "little")  # 2=WIN_CERT_TYPE_PKCS_SIGNED_DATA
        blob = data[offset + 8: offset + dw_length]

        if cert_type == 2:
            certs = pkcs7.load_der_pkcs7_certificates(blob)

            def has_code_signing_eku(c: x509.Certificate) -> bool:
                # Code Signing EKU を持つ証明書を優先(なければ先頭)
                try:
                    eku = c.extensions.get_extension_for_oid(
                        x509.oid.ExtensionOID.EXTENDED_KEY_USAGE
                    ).value
                    return x509.oid.ExtendedKeyUsageOID.CODE_SIGNING in eku
                except x509.ExtensionNotFound:
                    return False

            target = next((c for c in certs if has_code_signing_eku(c)), certs[0] if certs else None)
            if target:
                print("Subject:", target.subject.rfc4514_string())
                print("Issuer :", target.issuer.rfc4514_string())
                thumbprint = target.fingerprint(hashes.SHA1()).hex().upper()
                print("Thumbprint (SHA1):", thumbprint)
                break  # 目的がThumbprint取得ならここで十分

        # 8バイト境界にアライン
        offset = (offset + ((dw_length + 7) & ~7))

    if not thumbprint:
        raise ValueError("証明書を取得できませんでした。")
    return thumbprint


if __name__ == "__main__":
    exe_path = "dist/myapp_server.exe"
    try:
        thumb = get_exe_thumbprint(exe_path)
        print("Thumbprint取得成功:", thumb)
    except Exception as e:
        tb = sys.exc_info()[2]
        print("署名取得エラー:{}".format(e.with_traceback(tb)))

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2025-12-01 (月) 08:22:04 (4d)