TLS証明書管理 - ha1t/php-gm-server GitHub Wiki
本ページでは、php-gm-server における TLS 証明書の自動生成・管理方法、ディレクトリ構成、環境変数による制御、および開発環境での自己署名証明書の使用について説明します。Gemini Protocol は TLS/SSL の使用が必須であり、サーバーはクライアント接続時に証明書を提示する必要があります。本実装は開発環境での利便性を重視し、初回起動時に自動的に自己署名証明書を生成するアプローチを採用しています。
Gemini Protocol は安全な通信を保証するため、すべての通信を TLS で暗号化することが仕様で定められています。php-gm-server は起動時に自動的に TLS 用の自己署名証明書を生成し、そのパス情報を React PHP の SecureServer に渡すことで、安全な Gemini サーバーを実現しています。
この設計の主な特徴は以下の通りです。
- 自動生成: サーバー初回起動時に証明書が存在しない場合、自動的に生成される
-
ホスト名カスタマイズ: 環境変数
GEMINI_HOSTNAMEで証明書の CommonName を指定可能 -
ディレクトリ制御: 環境変数
GEMINI_CERT_DIRで証明書の保存先を指定可能 - 既存証明書の再利用: 一度生成された証明書は、以後のサーバー起動で再利用される
- 自己署名: 開発環境での簡易な運用を想定し、自己署名証明書を採用
CertificateGenerator クラスは、自己署名 TLS 証明書の生成と管理を担当するコンポーネントです。OpenSSL 拡張を使用して RSA 秘密鍵と証明書署名要求(CSR)を生成し、1年間有効な証明書を作成します。
classDiagram
class CertificateGenerator {
-string certDir
+__construct(string certDir)
+generate(string hostname) string
}
Sources: php-gm-server/src/CertificateGenerator.php:7-51
CertificateGenerator のコンストラクタは、証明書の保存先ディレクトリパスを受け取ります。
public function __construct(string $certDir)
{
$this->certDir = $certDir;
}Sources: php-gm-server/src/CertificateGenerator.php:11-14
generate() メソッドは、指定されたホスト名に対応する自己署名証明書を生成または取得します。既存の証明書ファイルが存在する場合は、そのパスを返却し、重複生成を回避します。
public function generate(string $hostname): string
{
$pemPath = $this->certDir . '/server.pem';
if (file_exists($pemPath)) {
return $pemPath;
}
if (!is_dir($this->certDir)) {
mkdir($this->certDir, 0755, true);
}
$privateKey = openssl_pkey_new([
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
$dn = [
'commonName' => $hostname,
];
$csr = openssl_csr_new($dn, $privateKey);
$cert = openssl_csr_sign($csr, null, $privateKey, 365);
$certPem = '';
openssl_x509_export($cert, $certPem);
$keyPem = '';
openssl_pkey_export($privateKey, $keyPem);
file_put_contents($pemPath, $certPem . $keyPem);
chmod($pemPath, 0600);
return $pemPath;
}Sources: php-gm-server/src/CertificateGenerator.php:16-50
生成処理の流れは以下の通りです。
-
既存確認:
certs/server.pemが既に存在するかを確認し、存在すれば直ちにそのパスを返却 - ディレクトリ作成: 保存先ディレクトリが存在しない場合、再帰的に作成(パーミッション 0755)
- 秘密鍵生成: OpenSSL API で RSA 2048 ビット秘密鍵を生成
- Distinguished Name 設定: 証明書の CommonName をホスト名に設定
- CSR 生成: 秘密鍵と DN 情報から証明書署名要求(Certificate Signing Request)を生成
- 証明書署名: CSR を秘密鍵で自己署名し、365日間有効な証明書を生成
- PEM 形式エクスポート: 証明書と秘密鍵を PEM 形式でテキスト化
-
ファイル保存: 証明書と秘密鍵を同一の
server.pemファイルに連結して保存 - パーミッション設定: ファイルのパーミッションを 0600(所有者のみ読み取り可能)に設定
| 項目 | 値 |
|---|---|
| タイプ | X.509 自己署名証明書 |
| 秘密鍵 | RSA 2048 ビット |
| 有効期限 | 365 日(1年) |
| 署名アルゴリズム | RSA |
| CommonName | 環境変数 GEMINI_HOSTNAME で指定 |
| ファイル形式 | PEM(テキスト) |
| ファイルパス | certs/server.pem |
| ファイルパーミッション | 0600(-rw-------) |
Sources: php-gm-server/src/CertificateGenerator.php:28-47
サーバーのエントリーポイント bin/server.php では、起動時に環境変数からサーバー設定を読み込み、CertificateGenerator を用いて証明書を取得した後、GeminiServer に渡します。
$certDir = getenv('GEMINI_CERT_DIR') ?: __DIR__ . '/../certs';
$hostname = getenv('GEMINI_HOSTNAME') ?: 'localhost';
$certGenerator = new CertificateGenerator($certDir);
$certPath = $certGenerator->generate($hostname);
echo "Certificate: {$certPath}\n";
$fileHandler = new StaticFileHandler($docRoot);
$server = new GeminiServer($host, $port, $fileHandler, $certPath);
$server->run();Sources: php-gm-server/bin/server.php:14-23
この処理フローを図示すると以下の通りです。
sequenceDiagram
participant Start as サーバー起動
participant CertGen as CertificateGenerator
participant FS as ファイルシステム
participant GemServer as GeminiServer
participant SSL as React SSL Layer
Start->>CertGen: generate(hostname)
CertGen->>FS: server.pem 確認
alt 既存ファイル
FS-->>CertGen: パス返却
else 未作成
CertGen->>CertGen: RSA秘密鍵生成
CertGen->>CertGen: CSR生成
CertGen->>CertGen: 証明書生成(365日有効)
CertGen->>FS: PEM保存(0600)
FS-->>CertGen: 成功
end
CertGen-->>Start: certPath
Start->>GemServer: GeminiServer(certPath)
Start->>GemServer: run()
GemServer->>SSL: SecureServer(certPath)
SSL->>FS: 証明書ファイル読込
GeminiServer クラスは、React PHP の SecureServer に対して証明書パスと各種オプションを設定します。
$tcp = new TcpServer("{$this->host}:{$this->port}", $loop);
$server = new SecureServer($tcp, $loop, [
'local_cert' => $this->certPath,
'allow_self_signed' => true,
'verify_peer' => false,
]);Sources: php-gm-server/src/GeminiServer.php:35-40
設定オプションの詳細は以下の通りです。
| オプション | 値 | 説明 |
|---|---|---|
local_cert |
$this->certPath |
サーバー証明書ファイルのパス。CertificateGenerator::generate() から渡された値 |
allow_self_signed |
true |
自己署名証明書の使用を許可 |
verify_peer |
false |
クライアント証明書の検証を実施しない |
Sources: php-gm-server/src/GeminiServer.php:36-40
証明書は certs/ ディレクトリに保存されます。本ディレクトリは .gitignore に記載されており、バージョン管理の対象外です。
project-root/
├── certs/
│ └── server.pem # 生成された証明書と秘密鍵
├── src/
│ └── CertificateGenerator.php
├── bin/
│ └── server.php
├── .gitignore
└── ...
Sources: php-gm-server/.gitignore:2
各ファイルの詳細は以下の通りです。
| ファイル | 説明 | パーミッション |
|---|---|---|
certs/server.pem |
X.509 証明書と RSA 秘密鍵を連結したファイル(PEM形式) | 0600 |
証明書の生成と管理は、以下の環境変数で制御できます。
| 環境変数 | デフォルト | 説明 |
|---|---|---|
GEMINI_CERT_DIR |
__DIR__ . '/../certs' |
証明書の保存先ディレクトリパス。ディレクトリが存在しない場合は自動作成される |
GEMINI_HOSTNAME |
localhost |
証明書の CommonName(識別名)。クライアントに表示される認識名として機能 |
Sources: php-gm-server/bin/server.php:14-15
デフォルト設定での起動:
php bin/server.phpこの場合、証明書は certs/server.pem に保存され、CommonName は localhost となります。
カスタムホスト名での起動:
GEMINI_HOSTNAME=example.com php bin/server.phpこの場合、CommonName が example.com の証明書が生成されます。既存の証明書ファイルがある場合は再利用され、新規生成は行われません。
カスタムディレクトリでの起動:
GEMINI_CERT_DIR=/etc/gemini/certs GEMINI_HOSTNAME=my-server.local php bin/server.phpこの場合、証明書は /etc/gemini/certs/server.pem に保存されます。
証明書は生成時から365日間有効です。OpenSSL の openssl_csr_sign() 関数の第4引数で指定されています。
$cert = openssl_csr_sign($csr, null, $privateKey, 365);Sources: php-gm-server/src/CertificateGenerator.php:38
有効期限切れの場合、クライアントが新しい証明書の生成が必要になります。既存の certs/server.pem ファイルを削除することで、次回サーバー起動時に新しい証明書が自動生成されます。
開発環境では、証明書認局(CA)から署名を取得することなく、自己署名証明書を使用します。これは以下のメリットがあります。
- 即座の利用: 認局への申請を待つ必要がなく、サーバーをすぐに起動できる
- コスト削減: 商用証明書の購入費が不要
- 開発効率: ホスト名やポート番号の変更時に新規証明書を簡単に生成できる
一方、クライアント側で信頼チェーンの検証ができないため、クライアントに対して証明書を事前に認識させるか、警告を無視する設定が必要です。
Gemini Protocol は TOFU(Trust On First Use)モデルを採用しています。このモデルでは、初回接続時にサーバーから提示された証明書を信頼できるものとして登録し、以後同じサーバーへの接続時には、その登録済み証明書を検証することで安全性を確保します。
sequenceDiagram
participant Client as Geminiクライアント
participant Server as php-gm-server
participant Store as クライアント証明書ストア
Client->>Server: TLS接続(初回)
Server-->>Client: 自己署名証明書を提示
Client->>Client: 証明書を検証できない<br/>警告を表示
Client->>Store: ユーザー許可後、<br/>証明書を登録
Client->>Server: TLS接続(2回目以降)
Server-->>Client: 同じ証明書を提示
Client->>Store: ストアと照合
Store-->>Client: 一致確認
Client->>Client: 警告なしで接続
多くの Gemini クライアント(例:Amfora、Kristall)は TOFU 機構を備えており、初回接続時に証明書を登録するか確認し、以後そのホスト/ポート組み合わせに対する接続時に同じ証明書を検証します。
CertificateGeneratorTest クラスでは、証明書生成機能を検証するユニットテストが実装されています。
public function testGenerateCreatesCertAndKey(): void
{
$generator = new CertificateGenerator($this->certDir);
$pemPath = $generator->generate('localhost');
$this->assertFileExists($pemPath);
$content = file_get_contents($pemPath);
$this->assertStringContainsString('-----BEGIN CERTIFICATE-----', $content);
$this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $content);
}
public function testGenerateReusesExisting(): void
{
$generator = new CertificateGenerator($this->certDir);
$path1 = $generator->generate('localhost');
$content1 = file_get_contents($path1);
$path2 = $generator->generate('localhost');
$content2 = file_get_contents($path2);
$this->assertSame($content1, $content2);
}Sources: php-gm-server/tests/CertificateGeneratorTest.php:28-49
テストは以下の2つのシナリオを検証します。
- 証明書生成テスト: 新規に証明書が生成され、PEM 形式の証明書と秘密鍵がファイルに含まれることを確認
-
証明書再利用テスト: 同じホスト名で
generate()を複数回呼び出した場合、既存ファイルが再利用されることを確認
証明書生成には PHP の OpenSSL 拡張が必須です。系統によるインストール方法は以下の通りです。
| 環境 | インストール方法 |
|---|---|
| Ubuntu/Debian | sudo apt-get install php-openssl |
| CentOS/RHEL | sudo yum install php-openssl |
| macOS(Homebrew) | brew install php |
| Docker | docker-php-ext-install openssl |
server.pem ファイルには秘密鍵が含まれるため、ファイルパーミッションは 0600(所有者のみ読み取り可能)に設定されます。本ファイルの内容を第三者に漏露させてはいけません。
Sources: php-gm-server/src/CertificateGenerator.php:47
開発環境では自己署名証明書で十分ですが、本番環境では以下の対応を推奨します。
- 商用 CA の利用: Let's Encrypt などの認局から署名済み証明書を取得
- 証明書更新の自動化: 有効期限の切れる前に新規証明書を自動取得・更新
- 証明書管理ツールの導入: Certbot などのツールで証明書ライフサイクルを管理
現在の実装では verify_peer が false に設定されており、クライアント証明書の検証は行われていません。これは Gemini プロトコルの仕様に合致しており、認証が必要な場合は別の仕組み(例:パスワード認証、トークンベース認証)を検討してください。
Sources: php-gm-server/src/GeminiServer.php:39
- プロジェクト概要 — PHP GM Server のプロジェクト全体像、目的、技術スタック
- インストール・セットアップガイド — 初回起動時の自動証明書生成の流れ
- 環境変数設定リファレンス — GEMINI_CERT_DIR と GEMINI_HOSTNAME の詳細
- GeminiServerコンポーネント — TLS接続の確立と SecureServer の設定
- CertificateGeneratorコンポーネント — 証明書生成の詳細実装
- セキュリティ設計 — TLS/SSL と TOFU モデルに基づくセキュリティアーキテクチャ