[PHP]stream_socket_enable_cryptoで動的にTLS暗号化を有効化する|STARTTLSパターンから証明書検証まで実践ガイド

PHP

はじめに

PHPでTLS暗号化通信を行う場合、ssl:// スキームで最初から暗号化した接続を確立するのが一般的です。しかし、SMTPやFTPのような プロトコルでは、最初は平文で接続し、途中から暗号化を開始する 「STARTTLS」というパターンが広く使われています。

stream_socket_enable_crypto は、既存のストリームに対して後からTLS暗号化を有効・無効にするための関数です。平文で接続を確立した後に暗号化を昇格させたり、逆に暗号化を解除したりする高度な制御が可能です。

この記事では、基本的な使い方から実践的なSTARTTLS実装まで、クラスを用いた具体例とともに丁寧に解説します。


stream_socket_enable_crypto とは

項目内容
関数名stream_socket_enable_crypto
PHPバージョンPHP 5.1.0以降
カテゴリストリーム関数
返り値true(成功)、false(失敗)、0(ノンブロッキング時に未完了)

構文

stream_socket_enable_crypto(
    resource $stream,
    bool     $enable,
    ?int     $crypto_method = null,
    ?resource $session_stream = null
): int|bool

パラメータ

パラメータ説明
$streamresource対象のストリームリソース
$enablebooltrue で暗号化有効、false で無効(平文に戻す)
$crypto_method?int使用する暗号化方式(下表参照)。$enable=true 時は必須
$session_stream?resourceTLSセッションを再利用する場合の既存ストリーム

暗号化方式の定数

定数説明
STREAM_CRYPTO_METHOD_TLS_CLIENTTLS(クライアント側、バージョン自動選択)
STREAM_CRYPTO_METHOD_TLS_SERVERTLS(サーバー側、バージョン自動選択)
STREAM_CRYPTO_METHOD_TLSv1_0_CLIENTTLS 1.0(クライアント)
STREAM_CRYPTO_METHOD_TLSv1_1_CLIENTTLS 1.1(クライアント)
STREAM_CRYPTO_METHOD_TLSv1_2_CLIENTTLS 1.2(クライアント)
STREAM_CRYPTO_METHOD_TLSv1_3_CLIENTTLS 1.3(クライアント、PHP 7.4以降)
STREAM_CRYPTO_METHOD_SSLv23_CLIENTSSL 2/3(非推奨・レガシー)

返り値

意味
true暗号化ネゴシエーション成功
falseネゴシエーション失敗
0ノンブロッキングモード時、まだ完了していない(再試行が必要)

0 に注意: ノンブロッキングモードでは true ではなく 0 が返ることがあります。=== true で厳密に判定してください。


STARTTLSの仕組み

【通常のTLS(ssl://)】
クライアント                    サーバー
    ├── TCP接続 ─────────────→  ├
    ├── TLSハンドシェイク ────→  ├  ← 最初から暗号化
    ├── 暗号化通信 ───────────→  ├

【STARTTLS(stream_socket_enable_crypto を使う)】
クライアント                    サーバー
    ├── TCP接続 ─────────────→  ├
    ├── 平文で "EHLO" ────────→  ├  ← 最初は平文
    ├── "STARTTLS" 送信 ──────→  ├
    ←── "220 Ready" 受信 ──────  ├
    ├── stream_socket_enable_crypto($s, true, ...) 呼び出し
    ├── TLSハンドシェイク ────→  ├  ← ここから暗号化
    ├── 暗号化通信 ───────────→  ├

基本的な使い方

<?php
// 平文でTCP接続
$socket = stream_socket_client('tcp://smtp.example.com:587', $errno, $errstr, 5.0);

if ($socket === false) {
    die("接続失敗: [{$errno}] {$errstr}");
}

// 平文でサーバーのバナーを受信
$banner = fgets($socket);
echo "バナー: {$banner}";

// STARTTLS開始
fwrite($socket, "STARTTLS\r\n");
$response = fgets($socket);
echo "応答: {$response}";

// TLS暗号化を有効化
$result = stream_socket_enable_crypto(
    $socket,
    true,
    STREAM_CRYPTO_METHOD_TLS_CLIENT
);

if ($result === true) {
    echo "TLS有効化成功" . PHP_EOL;
    // 以降は暗号化通信
} elseif ($result === false) {
    echo "TLS有効化失敗" . PHP_EOL;
} else {
    echo "TLS有効化未完了(ノンブロッキング)" . PHP_EOL;
}

fclose($socket);

実践例(クラスを使った実装)

例1:STARTTLSを実装したSMTPクライアント

メール送信で使われるSMTPの587番ポートで、平文接続からTLSへの昇格を実装します。

<?php

class SmtpStartTlsClient
{
    private $socket    = null;
    private string $host;
    private int    $port;
    private bool   $tlsEnabled  = false;
    private array  $sessionLog  = [];

    public function __construct(string $host, int $port = 587)
    {
        $this->host = $host;
        $this->port = $port;
    }

    public function connect(): void
    {
        // まず平文でTCP接続
        $this->socket = stream_socket_client(
            "tcp://{$this->host}:{$this->port}",
            $errno,
            $errstr,
            10.0,
            STREAM_CLIENT_CONNECT
        );

        if ($this->socket === false) {
            throw new RuntimeException("接続失敗 [{$errno}]: {$errstr}");
        }

        stream_set_timeout($this->socket, 10);

        // サーバーバナーを受信
        $banner = $this->readLine();
        $this->log('S', $banner);

        if (!str_starts_with($banner, '220')) {
            throw new RuntimeException("予期しないバナー: {$banner}");
        }
    }

    public function ehlo(string $domain = 'localhost'): array
    {
        $this->writeLine("EHLO {$domain}");
        $this->log('C', "EHLO {$domain}");

        $capabilities = [];
        while (true) {
            $line = $this->readLine();
            $this->log('S', $line);

            $capabilities[] = substr($line, 4);

            // 最終行はスペースの代わりにハイフン以外
            if (isset($line[3]) && $line[3] !== '-') {
                break;
            }
        }

        return $capabilities;
    }

    /**
     * STARTTLS コマンドを送信し、TLS暗号化を有効化する
     */
    public function startTls(): void
    {
        $this->writeLine("STARTTLS");
        $this->log('C', "STARTTLS");

        $response = $this->readLine();
        $this->log('S', $response);

        if (!str_starts_with($response, '220')) {
            throw new RuntimeException("STARTTLSが拒否されました: {$response}");
        }

        // SSLコンテキストを設定
        $context = stream_context_create([
            'ssl' => [
                'verify_peer'      => true,
                'verify_peer_name' => true,
                'peer_name'        => $this->host,
                'cafile'           => '/etc/ssl/certs/ca-certificates.crt',
            ],
        ]);
        stream_context_set_option($this->socket, stream_context_get_options($context));

        // TLS暗号化を有効化
        $result = stream_socket_enable_crypto(
            $this->socket,
            true,
            STREAM_CRYPTO_METHOD_TLS_CLIENT
        );

        if ($result !== true) {
            throw new RuntimeException(
                "TLSハンドシェイク失敗" . ($result === false ? '' : '(未完了)')
            );
        }

        $this->tlsEnabled = true;
        $this->log('*', "TLS有効化成功");
    }

    public function auth(string $user, string $password): bool
    {
        $this->writeLine("AUTH LOGIN");
        $this->log('C', "AUTH LOGIN");
        $challenge1 = $this->readLine();
        $this->log('S', $challenge1); // 334 VXNlcm5hbWU6

        $this->writeLine(base64_encode($user));
        $challenge2 = $this->readLine();
        $this->log('S', $challenge2); // 334 UGFzc3dvcmQ6

        $this->writeLine(base64_encode($password));
        $result = $this->readLine();
        $this->log('S', $result);

        return str_starts_with($result, '235');
    }

    public function quit(): void
    {
        $this->writeLine("QUIT");
        $this->log('C', "QUIT");
        $resp = $this->readLine();
        $this->log('S', $resp);
        fclose($this->socket);
        $this->socket = null;
    }

    public function isTlsEnabled(): bool
    {
        return $this->tlsEnabled;
    }

    public function printLog(): void
    {
        echo "=== SMTPセッションログ ===" . PHP_EOL;
        foreach ($this->sessionLog as $entry) {
            $prefix = match($entry['dir']) {
                'C' => "\e[32mC>\e[0m",  // 緑: クライアント送信
                'S' => "\e[34mS<\e[0m",  // 青: サーバー受信
                '*' => "\e[33m**\e[0m",  // 黄: 内部イベント
                default => '??',
            };
            echo "  [{$entry['time']}] {$prefix} {$entry['message']}" . PHP_EOL;
        }
    }

    private function writeLine(string $line): void
    {
        fwrite($this->socket, $line . "\r\n");
    }

    private function readLine(): string
    {
        $line = fgets($this->socket, 4096);
        $meta = stream_get_meta_data($this->socket);
        if ($meta['timed_out']) {
            throw new RuntimeException("受信タイムアウト");
        }
        return $line !== false ? rtrim($line) : '';
    }

    private function log(string $dir, string $message): void
    {
        $this->sessionLog[] = [
            'dir'     => $dir,
            'message' => $message,
            'time'    => date('H:i:s'),
        ];
    }
}

// 使用例
$smtp = new SmtpStartTlsClient('smtp.gmail.com', 587);

try {
    $smtp->connect();
    $caps = $smtp->ehlo('myapp.example.com');

    echo "サーバー機能: " . implode(', ', array_slice($caps, 0, 5)) . PHP_EOL;

    if (in_array('STARTTLS', $caps)) {
        $smtp->startTls();
        echo "TLS有効: " . ($smtp->isTlsEnabled() ? 'はい' : 'いいえ') . PHP_EOL;

        // 認証(実際の認証情報が必要)
        // $smtp->auth('user@gmail.com', 'app_password');
    } else {
        echo "このサーバーはSTARTTLSに対応していません" . PHP_EOL;
    }

    $smtp->quit();
} catch (RuntimeException $e) {
    echo "エラー: " . $e->getMessage() . PHP_EOL;
}
// $smtp->printLog();

例2:TLS暗号化の有効/無効を動的に切り替えるラッパー

stream_socket_enable_crypto の返り値(true / false / 0)を安全にハンドリングするラッパークラスです。

<?php

class CryptoStreamWrapper
{
    private $stream;
    private string $label;
    private bool   $cryptoEnabled = false;
    private array  $cryptoLog     = [];
    private int    $cryptoMethod;

    public function __construct(
        resource $stream,
        string   $label        = 'stream',
        int      $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT
    ) {
        $this->stream       = $stream;
        $this->label        = $label;
        $this->cryptoMethod = $cryptoMethod;
    }

    /**
     * TLS暗号化を有効化する
     * ノンブロッキングモード時は完了まで再試行する
     */
    public function enableCrypto(int $maxRetries = 10, int $retryUsec = 50_000): void
    {
        $attempt = 0;

        while ($attempt < $maxRetries) {
            $result = stream_socket_enable_crypto(
                $this->stream,
                true,
                $this->cryptoMethod
            );

            if ($result === true) {
                $this->cryptoEnabled = true;
                $this->record('enabled', $attempt + 1);
                return;
            }

            if ($result === false) {
                $this->record('failed', $attempt + 1);
                throw new RuntimeException("[{$this->label}] TLS有効化失敗");
            }

            // $result === 0: ノンブロッキングで未完了 → 再試行
            $attempt++;
            usleep($retryUsec);
        }

        $this->record('timeout', $attempt);
        throw new RuntimeException("[{$this->label}] TLS有効化タイムアウト({$maxRetries}回試行)");
    }

    /**
     * TLS暗号化を無効化する(平文に戻す)
     */
    public function disableCrypto(): void
    {
        $result = stream_socket_enable_crypto($this->stream, false);

        if ($result === true || $result === 0) {
            $this->cryptoEnabled = false;
            $this->record('disabled', 1);
        } else {
            $this->record('disable_failed', 1);
            throw new RuntimeException("[{$this->label}] TLS無効化失敗");
        }
    }

    public function write(string $data): int
    {
        $mode    = $this->cryptoEnabled ? '🔒' : '📭';
        $written = fwrite($this->stream, $data);
        echo "{$mode} 送信: " . addcslashes(substr($data, 0, 40), "\r\n") . PHP_EOL;
        return $written ?: 0;
    }

    public function readLine(): string
    {
        $line = fgets($this->stream, 4096);
        $mode = $this->cryptoEnabled ? '🔒' : '📭';
        $out  = $line !== false ? rtrim($line) : '';
        echo "{$mode} 受信: {$out}" . PHP_EOL;
        return $out;
    }

    public function isCryptoEnabled(): bool
    {
        return $this->cryptoEnabled;
    }

    public function getStream(): resource
    {
        return $this->stream;
    }

    public function printLog(): void
    {
        echo PHP_EOL . "=== 暗号化操作ログ [{$this->label}] ===" . PHP_EOL;
        foreach ($this->cryptoLog as $entry) {
            echo "  [{$entry['time']}] {$entry['event']} (試行: {$entry['attempts']}回)" . PHP_EOL;
        }
    }

    private function record(string $event, int $attempts): void
    {
        $this->cryptoLog[] = [
            'event'    => $event,
            'attempts' => $attempts,
            'time'     => date('H:i:s'),
        ];
    }
}

// 使用例(FTP ALTHでのSTARTTLSに似たシナリオ)
$socket = stream_socket_client('tcp://ftp.example.com:21', $errno, $errstr, 5.0);

if ($socket) {
    $wrapper = new CryptoStreamWrapper($socket, 'ftp.example.com', STREAM_CRYPTO_METHOD_TLS_CLIENT);
    stream_set_timeout($socket, 5);

    $banner = $wrapper->readLine(); // 220 FTP Server Ready

    $wrapper->write("AUTH TLS\r\n");
    $response = $wrapper->readLine(); // 234 AUTH TLS OK

    if (str_starts_with($response, '234')) {
        try {
            $wrapper->enableCrypto();
            echo "暗号化状態: " . ($wrapper->isCryptoEnabled() ? '有効' : '無効') . PHP_EOL;
        } catch (RuntimeException $e) {
            echo "エラー: " . $e->getMessage() . PHP_EOL;
        }
    }

    $wrapper->printLog();
    fclose($socket);
}

例3:証明書検証オプションを細かく制御するTLSコンフィギュレーター

stream_context_create の SSL オプションと stream_socket_enable_crypto を組み合わせて、証明書検証を柔軟に制御します。

<?php

class TlsConfigurator
{
    private array $sslOptions;
    private int   $cryptoMethod;

    public function __construct(
        bool   $verifyPeer      = true,
        bool   $verifyPeerName  = true,
        bool   $allowSelfSigned = false,
        int    $cryptoMethod    = STREAM_CRYPTO_METHOD_TLS_CLIENT,
        string $caFile          = ''
    ) {
        $this->cryptoMethod = $cryptoMethod;
        $this->sslOptions   = [
            'verify_peer'       => $verifyPeer,
            'verify_peer_name'  => $verifyPeerName,
            'allow_self_signed' => $allowSelfSigned,
            'SNI_enabled'       => true,
        ];

        if ($caFile !== '') {
            $this->sslOptions['cafile'] = $caFile;
        }
    }

    public function withClientCert(string $certFile, string $keyFile, string $passphrase = ''): static
    {
        $this->sslOptions['local_cert'] = $certFile;
        $this->sslOptions['local_pk']   = $keyFile;
        if ($passphrase !== '') {
            $this->sslOptions['passphrase'] = $passphrase;
        }
        return $this;
    }

    public function withPeerName(string $peerName): static
    {
        $this->sslOptions['peer_name'] = $peerName;
        return $this;
    }

    /**
     * 既存のストリームにコンテキストを適用してTLSを有効化する
     */
    public function applyTo(resource $stream): array
    {
        // コンテキストをストリームに適用
        stream_context_set_option($stream, ['ssl' => $this->sslOptions]);

        $startTime = microtime(true);
        $result    = stream_socket_enable_crypto($stream, true, $this->cryptoMethod);
        $elapsed   = microtime(true) - $startTime;

        if ($result === false) {
            // OpenSSLエラーを取得
            $error = '';
            while ($msg = openssl_error_string()) {
                $error .= $msg . '; ';
            }
            throw new RuntimeException("TLS有効化失敗: " . rtrim($error, '; '));
        }

        // 暗号化情報を取得
        $meta   = stream_get_meta_data($stream);
        $crypto = $meta['crypto'] ?? [];

        return [
            'success'       => ($result === true),
            'protocol'      => $crypto['protocol']         ?? 'N/A',
            'cipher_name'   => $crypto['cipher_name']      ?? 'N/A',
            'cipher_bits'   => $crypto['cipher_bits']      ?? 0,
            'cipher_version'=> $crypto['cipher_version']   ?? 'N/A',
            'handshake_ms'  => round($elapsed * 1000, 2),
            'ssl_options'   => $this->sslOptions,
        ];
    }

    public function describe(): string
    {
        $parts = [];
        $parts[] = "verify_peer="     . ($this->sslOptions['verify_peer']       ? 'true' : 'false');
        $parts[] = "verify_peer_name="  . ($this->sslOptions['verify_peer_name']  ? 'true' : 'false');
        $parts[] = "allow_self_signed=" . ($this->sslOptions['allow_self_signed'] ? 'true' : 'false');
        return implode(', ', $parts);
    }
}

// 使用例
$socket = stream_socket_client('tcp://www.example.com:443', $errno, $errstr, 5.0);

if ($socket) {
    stream_set_timeout($socket, 5);

    $config = (new TlsConfigurator(
        verifyPeer:      true,
        verifyPeerName:  true,
        allowSelfSigned: false,
        cryptoMethod:    STREAM_CRYPTO_METHOD_TLS_CLIENT
    ))->withPeerName('www.example.com');

    echo "TLS設定: " . $config->describe() . PHP_EOL;

    try {
        $info = $config->applyTo($socket);

        echo "プロトコル    : {$info['protocol']}"         . PHP_EOL;
        echo "暗号スイート  : {$info['cipher_name']}"      . PHP_EOL;
        echo "鍵長          : {$info['cipher_bits']} bits" . PHP_EOL;
        echo "ハンドシェイク: {$info['handshake_ms']} ms"  . PHP_EOL;

        // TLS確立後にHTTPリクエスト
        fwrite($socket, "GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n");
        $status = fgets($socket);
        echo "HTTP          : " . trim($status)             . PHP_EOL;

    } catch (RuntimeException $e) {
        echo "エラー: " . $e->getMessage() . PHP_EOL;
    }

    fclose($socket);
}

出力例:

TLS設定: verify_peer=true, verify_peer_name=true, allow_self_signed=false
プロトコル    : TLSv1.3
暗号スイート  : TLS_AES_256_GCM_SHA384
鍵長          : 256 bits
ハンドシェイク: 43.21 ms
HTTP          : HTTP/1.0 200 OK

例4:TLSサーバー側 ─ クライアント接続後に暗号化を有効化する

サーバー側でも stream_socket_enable_crypto を使い、クライアント接続を受け付けた後に暗号化します。

<?php

class StarttlsCapableServer
{
    private $server;
    private string $certFile;
    private string $keyFile;
    private int    $port;

    public function __construct(int $port, string $certFile, string $keyFile)
    {
        $this->port     = $port;
        $this->certFile = $certFile;
        $this->keyFile  = $keyFile;

        // 平文でリスニング(ssl:// ではなく tcp://)
        $this->server = stream_socket_server(
            "tcp://0.0.0.0:{$port}",
            $errno,
            $errstr
        );

        if (!$this->server) {
            throw new RuntimeException("サーバー起動失敗 [{$errno}]: {$errstr}");
        }

        echo "STARTTLSサーバー起動: :{$port}" . PHP_EOL;
    }

    public function acceptOne(float $timeout = 30.0): void
    {
        $client = stream_socket_accept($this->server, $timeout, $peerName);

        if ($client === false) {
            echo "タイムアウト" . PHP_EOL;
            return;
        }

        echo "接続: {$peerName}" . PHP_EOL;
        stream_set_timeout($client, 10);

        // 平文でバナー送信
        fwrite($client, "220 STARTTLS対応サーバー\r\n");

        $cmd = rtrim(fgets($client));
        echo "受信(平文): {$cmd}" . PHP_EOL;

        if (strtoupper($cmd) === 'STARTTLS') {
            fwrite($client, "220 STARTTLSを開始します\r\n");

            // サーバー側のSSLコンテキストを設定
            stream_context_set_option($client, 'ssl', 'local_cert', $this->certFile);
            stream_context_set_option($client, 'ssl', 'local_pk',   $this->keyFile);
            stream_context_set_option($client, 'ssl', 'verify_peer', false);

            // サーバー側でTLSを有効化
            $result = stream_socket_enable_crypto(
                $client,
                true,
                STREAM_CRYPTO_METHOD_TLS_SERVER
            );

            if ($result === true) {
                echo "TLS有効化成功(サーバー側)" . PHP_EOL;
                fwrite($client, "暗号化通信を開始しました\r\n");

                // 暗号化されたデータを受信
                $encData = fgets($client);
                echo "受信(暗号化): " . trim($encData) . PHP_EOL;
            } else {
                echo "TLS有効化失敗" . PHP_EOL;
            }
        }

        fclose($client);
    }

    public function close(): void
    {
        fclose($this->server);
    }
}

// 証明書ファイルが必要なため、クラス定義のみ表示
// 実際の使用例:
// $srv = new StarttlsCapableServer(587, '/path/to/cert.pem', '/path/to/key.pem');
// $srv->acceptOne(30.0);
// $srv->close();
echo "StarttlsCapableServer クラス定義完了" . PHP_EOL;
echo "(証明書ファイルを用意して使用してください)" . PHP_EOL;

例5:ノンブロッキングモードでTLSハンドシェイクを進めるポーラー

ノンブロッキングストリームで 0 が返り続ける状態を stream_select と組み合わせて効率的に処理します。

<?php

class NonBlockingTlsHandshaker
{
    private $stream;
    private int    $cryptoMethod;
    private int    $maxAttempts;
    private array  $handshakeLog = [];

    public function __construct(
        resource $stream,
        int      $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT,
        int      $maxAttempts  = 100
    ) {
        $this->stream       = $stream;
        $this->cryptoMethod = $cryptoMethod;
        $this->maxAttempts  = $maxAttempts;
    }

    /**
     * ノンブロッキングモードでTLSハンドシェイクを完了させる
     */
    public function handshake(float $timeoutSec = 5.0): bool
    {
        stream_set_blocking($this->stream, false);

        $deadline = microtime(true) + $timeoutSec;
        $attempt  = 0;

        while ($attempt < $this->maxAttempts && microtime(true) < $deadline) {
            $result = stream_socket_enable_crypto(
                $this->stream,
                true,
                $this->cryptoMethod
            );

            $this->handshakeLog[] = [
                'attempt' => ++$attempt,
                'result'  => $result,
                'time_ms' => round((microtime(true) - ($deadline - $timeoutSec)) * 1000, 2),
            ];

            if ($result === true) {
                // ハンドシェイク完了
                stream_set_blocking($this->stream, true);
                return true;
            }

            if ($result === false) {
                // ハンドシェイク失敗
                return false;
            }

            // $result === 0: 未完了 → stream_select で少し待つ
            $read    = [$this->stream];
            $write   = [$this->stream];
            $except  = null;
            stream_select($read, $write, $except, 0, 10_000); // 10ms待機
        }

        return false; // タイムアウト
    }

    public function printHandshakeLog(): void
    {
        echo "=== TLSハンドシェイクログ ===" . PHP_EOL;
        echo str_pad("試行回数", 10)
           . str_pad("結果",     12)
           . "経過時間" . PHP_EOL;
        echo str_repeat('-', 34) . PHP_EOL;

        foreach ($this->handshakeLog as $entry) {
            $resultStr = match(true) {
                $entry['result'] === true  => '✓ 完了',
                $entry['result'] === false => '✗ 失敗',
                $entry['result'] === 0     => '⋯ 未完了',
                default                    => '?',
            };
            echo str_pad($entry['attempt'], 10)
               . str_pad($resultStr, 12)
               . "{$entry['time_ms']} ms" . PHP_EOL;
        }

        $total = count($this->handshakeLog);
        echo "合計試行回数: {$total}" . PHP_EOL;
    }
}

// 使用例
$socket = stream_socket_client('tcp://www.example.com:443', $errno, $errstr, 5.0);

if ($socket) {
    $handshaker = new NonBlockingTlsHandshaker(
        $socket,
        STREAM_CRYPTO_METHOD_TLS_CLIENT,
        50
    );

    $success = $handshaker->handshake(5.0);

    if ($success) {
        echo "TLSハンドシェイク完了" . PHP_EOL;

        // ブロッキングに戻して通常通信
        fwrite($socket, "GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n");
        $line = fgets($socket);
        echo "HTTP: " . trim($line) . PHP_EOL;
    } else {
        echo "TLSハンドシェイク失敗" . PHP_EOL;
    }

    $handshaker->printHandshakeLog();
    fclose($socket);
}

出力例:

TLSハンドシェイク完了
HTTP: HTTP/1.0 200 OK
=== TLSハンドシェイクログ ===
試行回数  結果        経過時間
----------------------------------
1         ⋯ 未完了    12.34 ms
2         ⋯ 未完了    23.87 ms
3         ✓ 完了      35.12 ms
合計試行回数: 3

例6:TLSセッション再利用で高速ハンドシェイクを実現する

$session_stream パラメータを使い、既存接続のTLSセッションを新しい接続で再利用することでハンドシェイクを高速化します。

<?php

class TlsSessionReuseClient
{
    private string $host;
    private int    $port;
    private array  $timings = [];

    public function __construct(string $host, int $port = 443)
    {
        $this->host = $host;
        $this->port = $port;
    }

    /**
     * 通常のTLS接続を確立する
     */
    public function connectFresh(string $label = '新規'): resource
    {
        $socket = $this->openTcp();
        $start  = microtime(true);

        $result = stream_socket_enable_crypto(
            $socket,
            true,
            STREAM_CRYPTO_METHOD_TLS_CLIENT
        );

        $elapsed = microtime(true) - $start;

        if ($result !== true) {
            fclose($socket);
            throw new RuntimeException("TLS接続失敗");
        }

        $this->timings[$label] = round($elapsed * 1000, 2);
        return $socket;
    }

    /**
     * 既存セッションを再利用したTLS接続を確立する
     */
    public function connectWithSession(resource $sessionStream, string $label = 'セッション再利用'): resource
    {
        $socket = $this->openTcp();
        $start  = microtime(true);

        // $session_stream に既存の接続を渡すとセッションを再利用
        $result = stream_socket_enable_crypto(
            $socket,
            true,
            STREAM_CRYPTO_METHOD_TLS_CLIENT,
            $sessionStream  // ← セッション再利用
        );

        $elapsed = microtime(true) - $start;

        if ($result !== true) {
            fclose($socket);
            throw new RuntimeException("セッション再利用TLS接続失敗");
        }

        $this->timings[$label] = round($elapsed * 1000, 2);
        return $socket;
    }

    public function printTimings(): void
    {
        echo "=== TLSハンドシェイク時間比較 ===" . PHP_EOL;
        foreach ($this->timings as $label => $ms) {
            $bar = str_repeat('█', min((int)($ms / 5), 30));
            echo str_pad($label, 18) . ": {$bar} {$ms} ms" . PHP_EOL;
        }
    }

    private function openTcp(): resource
    {
        $socket = stream_socket_client(
            "tcp://{$this->host}:{$this->port}",
            $errno,
            $errstr,
            5.0
        );

        if ($socket === false) {
            throw new RuntimeException("TCP接続失敗 [{$errno}]: {$errstr}");
        }

        stream_set_timeout($socket, 5);
        return $socket;
    }
}

// 使用例
$client = new TlsSessionReuseClient('www.example.com', 443);

try {
    // 1回目:通常のフルハンドシェイク
    $sock1 = $client->connectFresh('1回目(新規)');

    // 2回目:セッション再利用(高速)
    $sock2 = $client->connectWithSession($sock1, '2回目(再利用)');

    // 3回目:さらに再利用
    $sock3 = $client->connectWithSession($sock1, '3回目(再利用)');

    $client->printTimings();

    fclose($sock1);
    fclose($sock2);
    fclose($sock3);

} catch (RuntimeException $e) {
    echo "エラー: " . $e->getMessage() . PHP_EOL;
}

出力例:

=== TLSハンドシェイク時間比較 ===
1回目(新規)     : ████████ 43.21 ms
2回目(再利用)   : ███ 18.54 ms
3回目(再利用)   : ██ 12.87 ms

関連する関数との比較

関数役割
stream_socket_enable_crypto既存ストリームのTLS暗号化を動的に切り替え
stream_socket_client('ssl://')最初からTLS暗号化で接続
stream_socket_server('ssl://')TLSサーバーソケットを作成
stream_context_createSSLオプション(証明書・検証設定)を作成
stream_get_meta_dataTLSプロトコル・暗号スイート情報を取得
openssl_error_stringOpenSSL由来のエラー詳細を取得

ssl:// 直接接続 vs stream_socket_enable_crypto

// ssl:// で最初からTLS(シンプルなHTTPS)
$socket = stream_socket_client('ssl://example.com:443', $e, $es, 5.0);
// → 接続確立と同時にTLSハンドシェイク

// stream_socket_enable_crypto(STARTTLSなど)
$socket = stream_socket_client('tcp://smtp.example.com:587', $e, $es, 5.0);
// 平文通信...
fwrite($socket, "STARTTLS\r\n");
fgets($socket);
$result = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
// → 任意のタイミングでTLSに昇格
観点ssl:// スキームstream_socket_enable_crypto
TLS開始タイミング接続時に即座任意のタイミング
STARTTLSパターン不可◎ 対応
コード量少ないやや多い
暗号化解除不可可(false を渡す)
主な用途HTTPS・LDAPS などSMTP/FTP のSTARTTLS

よくある注意点・落とし穴

1. 返り値は === true で厳密に判定する

0 はノンブロッキング未完了を意味します。true / false / 0 の3値を正しく区別してください。

$result = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);

// NG:0(未完了)も成功と判定してしまう
if ($result) { echo "成功"; }

// OK:厳密比較
if ($result === true) {
    echo "TLS有効化成功";
} elseif ($result === 0) {
    echo "未完了(ノンブロッキング)";
} else {
    echo "TLS有効化失敗";
}

2. $enable=true 時は $crypto_method が必須

$enable=false(無効化)時は省略できますが、有効化時は必ず指定してください。

// NG:有効化時に $crypto_method を省略
stream_socket_enable_crypto($socket, true); // エラー

// OK
stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);

// 無効化時は省略可
stream_socket_enable_crypto($socket, false);

3. SSLエラーは openssl_error_string で詳細を取得できる

TLS有効化失敗時に詳細なエラー原因を取得するには以下のようにします。

$result = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);

if ($result === false) {
    while ($error = openssl_error_string()) {
        echo "OpenSSLエラー: {$error}" . PHP_EOL;
    }
}

4. stream_context_set_option はTLS有効化より前に設定する

SSL設定(証明書・CA・検証フラグ)は stream_socket_enable_crypto を呼ぶに適用する必要があります。

// OK:先にコンテキスト設定
stream_context_set_option($socket, 'ssl', 'verify_peer', true);
stream_context_set_option($socket, 'ssl', 'peer_name', 'example.com');
$result = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);

// NG:TLS有効化後に設定しても遅い
$result = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
stream_context_set_option($socket, 'ssl', 'verify_peer', true); // 効果なし

まとめ

項目内容
関数名stream_socket_enable_crypto(resource $stream, bool $enable, ?int $crypto_method, ?resource $session_stream): int|bool
主な用途既存ストリームへのTLS暗号化の動的付与(STARTTLSパターン)
返り値true=成功、false=失敗、0=ノンブロッキング未完了
代表的な用途SMTP(587)・FTP(21)のSTARTTLS、動的なTLS昇格
注意点返り値は === true で判定、$crypto_method は有効化時に必須
PHP バージョンPHP 5.1.0 以上

stream_socket_enable_crypto は、「平文で始まり途中からTLSに昇格する」STARTTLSパターンを実現する唯一の関数です。ssl:// スキームで解決できないSMTPやFTPのような2フェーズ通信プロトコルでは不可欠な存在です。

返り値が true / false / 0 の3値になる点と、SSLコンテキストを事前に設定する必要がある点をしっかり押さえることで、堅牢な暗号化通信を実装できます。

タイトルとURLをコピーしました