TLS証明書管理 - ha1t/php-gm-server GitHub Wiki

TLS証明書管理

本ページでは、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 コンポーネント

CertificateGenerator クラスは、自己署名 TLS 証明書の生成と管理を担当するコンポーネントです。OpenSSL 拡張を使用して RSA 秘密鍵と証明書署名要求(CSR)を生成し、1年間有効な証明書を作成します。

クラス設計

classDiagram
    class CertificateGenerator {
        -string certDir
        +__construct(string certDir)
        +generate(string hostname) string
    }
Loading

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

生成処理の流れは以下の通りです。

  1. 既存確認: certs/server.pem が既に存在するかを確認し、存在すれば直ちにそのパスを返却
  2. ディレクトリ作成: 保存先ディレクトリが存在しない場合、再帰的に作成(パーミッション 0755)
  3. 秘密鍵生成: OpenSSL API で RSA 2048 ビット秘密鍵を生成
  4. Distinguished Name 設定: 証明書の CommonName をホスト名に設定
  5. CSR 生成: 秘密鍵と DN 情報から証明書署名要求(Certificate Signing Request)を生成
  6. 証明書署名: CSR を秘密鍵で自己署名し、365日間有効な証明書を生成
  7. PEM 形式エクスポート: 証明書と秘密鍵を PEM 形式でテキスト化
  8. ファイル保存: 証明書と秘密鍵を同一の server.pem ファイルに連結して保存
  9. パーミッション設定: ファイルのパーミッションを 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: 証明書ファイル読込
Loading

GeminiServer での証明書設定

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)から署名を取得することなく、自己署名証明書を使用します。これは以下のメリットがあります。

  • 即座の利用: 認局への申請を待つ必要がなく、サーバーをすぐに起動できる
  • コスト削減: 商用証明書の購入費が不要
  • 開発効率: ホスト名やポート番号の変更時に新規証明書を簡単に生成できる

一方、クライアント側で信頼チェーンの検証ができないため、クライアントに対して証明書を事前に認識させるか、警告を無視する設定が必要です。

クライアント側での信頼設定と TOFU モデル

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: 警告なしで接続
Loading

多くの 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つのシナリオを検証します。

  1. 証明書生成テスト: 新規に証明書が生成され、PEM 形式の証明書と秘密鍵がファイルに含まれることを確認
  2. 証明書再利用テスト: 同じホスト名で generate() を複数回呼び出した場合、既存ファイルが再利用されることを確認

OpenSSL 拡張の要件

証明書生成には 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_peerfalse に設定されており、クライアント証明書の検証は行われていません。これは Gemini プロトコルの仕様に合致しており、認証が必要な場合は別の仕組み(例:パスワード認証、トークンベース認証)を検討してください。

Sources: php-gm-server/src/GeminiServer.php:39

Related Pages

⚠️ **GitHub.com Fallback** ⚠️