PDF用電子署名の証明書を手作りで作成してみました

# 環境情報

- 使用OS：`Windows`
- システム環境：`PowerShell`, `node.js`, `python`

# OpenSSLインストール

### ダウンロード

ダウンロードサイト
https://slproweb.com/products/Win32OpenSSL.html

私は下記のファイルをダウンロードしました。

- Win64 OpenSSL v3.5.0

- 余談：最近WindowsはOpenSSLが標準搭載だと思っていましたが、そうではなさそうでした

### 環境変数設定

Windowsシステム環境変数（PATH）に`C:\Program Files\OpenSSL-Win64\bin`を追加

### 動作確認

```
openssl version

OpenSSL 3.5.0 8 Apr 2025 (Library: OpenSSL 3.5.0 8 Apr 2025)
```

# 電子署名の証明書を作成

### 注意
**下記の方法で作成した証明書はCAを利用しないので、**
**電子署名の有効性を検証したとき必ずINVALIDとなります。**
**ですので実務で使うには不適切だと思います。**
**CAを利用するように、修正をするつもりです。申し訳ございません。**

### 秘密鍵と証明書（有効期限365日）を作成

- `/CN=Your Name`の`Your Name`は署名者名を入力します
- `days 365`は有効期間ですので、日数はお好きに変更します

```
openssl req -x509 -newkey rsa:2048 -keyout private.key -out certificate.crt -days 365 -nodes -subj "/CN=Your Name"
```

### p12ファイルにまとめる

- p12ファイル：秘密鍵と証明書（公開鍵証明書）をセットでまとめて、パスワードで保護したファイル
- こちらで設定したパスワードはPDFに電子署名をするときに必要です

```
openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt
Enter Export Password:

Verifying - Enter Export Password:

```

# PDF作成APP（Node.js）

### PDFに署名をする

- `pdfBuffer`はPDFデータです
- `passphrase: '****'`の`****`にはp12ファイル作成時に設定したパスワードを入力します

```typescript
import fs from "fs";
import { plainAddPlaceholder } from 'node-signpdf';
import signPdfInstance from 'node-signpdf';
//・・・中略・・・

// 電子署名用証明書ファイルのパス
const p12Path = path.join(process.cwd(), 'certificate.p12');
const p12Buffer = fs.readFileSync(p12Path);
// Buffer型に変換
const pdfBufferNode = Buffer.from(pdfBuffer);
// プレースホルダー追加
const pdfWithPlaceholder = plainAddPlaceholder({ pdfBuffer: pdfBufferNode, reason: '電子署名' });
// 署名付与（パスワードなしでp12を作成した場合は空文字を指定）
const signedPdf = signPdfInstance.sign(pdfWithPlaceholder, p12Buffer, { passphrase: '****' });

//・・・中略・・・
```

# PDFの署名を検証（Python）

- CAで作ってないためCAは確認しない
    - CAとは：認証局 (Certificate Authorities)
- ~~PDFファイルの`SHA256 fingerprint`と`Serial number`を確認する~~
    - ~~技術的には、元の証明書なしで`SHA256 fingerprint`と`Serial number`を偽造するのは不可能に近いらしいです~~
    - 重大な確認が必要な際は、万が一のため、PDFが本物なのかを電話などで確認した方がいいです

- 2025/06/11に追記
    - `SHA256 fingerprint`と`Serial number`の確認だけでは不十分でした
    - PDFが改ざんされたかをハッシューデータを作って検証しないといけない

### 証明書の署名情報を確認

- Passwordは`certificate.p12`作成時のパスワード

```
openssl pkcs12 -in certificate.p12 -nokeys -clcerts -out mycert.pem

Enter Import Password:

openssl x509 -in mycert.pem -noout -subject -issuer -serial -fingerprint -sha256

subject=CN=Park Test
issuer=CN=Park Test
serial=6D1B3ACBC48A1DCD20494D565AD63687C9E2D93D
sha256 Fingerprint=07:9B:94:AB:0B:0B:96:C6:80:C2:E2:EE:93:00:9C:8E:DF:6B:70:E4:EE:FC:8B:84:7E:0F:67:C2:53:CF:2E:DD
```

### PDFの署名を確認

```python
from pyhanko.pdf_utils.reader import PdfFileReader
from pyhanko.sign.validation import validate_pdf_signature
import contextlib
import io

with open("test2.pdf", "rb") as f:
    r = PdfFileReader(f)
    sig = r.embedded_signatures[0]
    # バリデーション時のstderr出力を抑制
    with contextlib.redirect_stderr(io.StringIO()):
        status = validate_pdf_signature(sig, key_usage_settings=None)
    cert = sig.signer_cert
    sha256 = cert.sha256_fingerprint  # Already a hex string
    sha1 = cert.sha1_fingerprint      # Already a hex string
    print("SHA256 fingerprint:", sha256)
    print("SHA1 fingerprint:", sha1)
    print("Subject:", cert.subject.human_friendly)
    # Adobeと同じ形式（0xなし大文字16進数）でシリアル番号を表示
    try:
        serial_bytes = cert.serial_number_bytes
        serial_hex = serial_bytes.hex().upper()
        print("Serial number:", serial_hex)
    except AttributeError:
        print("Serial number:", format(cert.serial_number, 'X'))
```

```
root@ccd3416ad175:/workspaces/python-tools# python sign-test.py
SHA256 fingerprint: 07 9B 94 AB 0B 0B 96 C6 80 C2 E2 EE 93 00 9C 8E DF 6B 70 E4 EE FC 8B 84 7E 0F 67 C2 53 CF 2E DD
SHA1 fingerprint: 7C 24 8F AA 04 07 16 C4 E0 BA 19 16 BE 08 14 07 A8 5D 45 96
Subject: Common Name: Park Test
Serial number: 6D1B3ACBC48A1DCD20494D565AD63687C9E2D93D
```

# ※注意

- 証明書のserialやfingerprintは公開して問題ありません
- 秘密鍵の内容は絶対に公開しないよう注意してください
- `certificate.p12`と`private.key`は公開してはいけません
- 自分で作成した証明書ではなく、認証局（CA）発行の`AATL`対応する証明書を利用すれば、AdobeなどでPDFの署名を検証した際、**信頼された署名**と表示されます
    - `AATL`：Adobe Approved Trust List
    - ※たとえCAが発行した証明書でも、`AATL`に対応していない場合は、Adobeで署名を確認しても「信頼できる」とは表示されません

# おわりに

ChatGPTとGeminiを活用して調査しました。