はじめに
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
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
$stream | resource | 対象のストリームリソース |
$enable | bool | true で暗号化有効、false で無効(平文に戻す) |
$crypto_method | ?int | 使用する暗号化方式(下表参照)。$enable=true 時は必須 |
$session_stream | ?resource | TLSセッションを再利用する場合の既存ストリーム |
暗号化方式の定数
| 定数 | 説明 |
|---|---|
STREAM_CRYPTO_METHOD_TLS_CLIENT | TLS(クライアント側、バージョン自動選択) |
STREAM_CRYPTO_METHOD_TLS_SERVER | TLS(サーバー側、バージョン自動選択) |
STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | TLS 1.0(クライアント) |
STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | TLS 1.1(クライアント) |
STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | TLS 1.2(クライアント) |
STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT | TLS 1.3(クライアント、PHP 7.4以降) |
STREAM_CRYPTO_METHOD_SSLv23_CLIENT | SSL 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_create | SSLオプション(証明書・検証設定)を作成 |
stream_get_meta_data | TLSプロトコル・暗号スイート情報を取得 |
openssl_error_string | OpenSSL由来のエラー詳細を取得 |
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コンテキストを事前に設定する必要がある点をしっかり押さえることで、堅牢な暗号化通信を実装できます。
