master
ブランチ master
LittleBeast / src / Std / Lib / Tls / Connection.php
68.4 KB、1516行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, and/or sell copies of the Software, subject to the following conditions:
10
11 1. **Origin Attribution**:
12 - You must not misrepresent the origin of the Software; you must not claim
13 you created the original Software.
14 - If the Software is used in a product, you must either:
15 a. Provide clear attribution in the product's documentation, user interface,
16 or other visible areas, **OR**
17 b. Pay the original developers a fee they specify in writing.
18 2. **Usage Restriction**:
19 - The Software, or any derivative works, dependencies, or libraries
20 incorporating it, must not be used for censorship or to suppress freedom of
21 speech, expression, or creativity. Prohibited uses include, but are not
22 limited to:
23 - Censorship of so-called "hate speech", visuals, non-mainstream opinions,
24 ideas, or objective reality.
25 - Tools or systems designed to restrict access to information or
26 artistic works.
27 3. **Notice Preservation**:
28 - This license and the above copyright notice must remain intact in all copies
29 of the source code.
30
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38namespace Std\Lib\Tls;
39
40use Std\Lib\Crypto\Aes;
41use Std\Lib\Crypto\ChaCha20;
42use Std\Lib\Crypto\Ec;
43use Std\Lib\Crypto\Rsa;
44use Std\Lib\Crypto\X25519;
45use Std\Lib\Crypto\X509;
46
47/**
48 * TLS 1.3(RFC 8446)とTLS 1.2(RFC 5246)の接続(クライアントとサーバー)
49 *
50 * 普通はStd\Lib\Tls(ストリームにする)経由で使う。
51 * TLS 1.3:TLS_AES_128_GCM_SHA256、TLS_AES_256_GCM_SHA384、TLS_CHACHA20_POLY1305_SHA256、
52 * HelloRetryRequest、KeyUpdate
53 * TLS 1.2:安全なスイートだけ(ECDHE+AEAD:AES-GCM、ChaCha20-Poly1305)、Extended Master Secret(RFC 7627)、
54 * 再ネゴシエーションは断る、TLS 1.3を使えるのに1.2に下げられた事(ダウングレード)を見つける
55 * 共通:x25519・secp256r1・secp384r1、RSA-PSS・ECDSAの署名(証明書とTLS 1.2はPKCS#1も)、ALPN、SNI
56 * 未対応:TLS 1.1以前、CBC・RSAの鍵交換・DHE、セッションの再開、0-RTT、クライアント証明書(求められたら空の証明書を送る)
57 */
58class Connection {
59 // レコードの種類
60 private const CCS = 20;
61 private const ALERT = 21;
62 private const HANDSHAKE = 22;
63 private const APPDATA = 23;
64
65 // ハンドシェイクの種類
66 private const HELLO_REQUEST = 0;
67 private const CLIENT_HELLO = 1;
68 private const SERVER_HELLO = 2;
69 private const NEW_SESSION_TICKET = 4;
70 private const ENCRYPTED_EXTENSIONS = 8;
71 private const CERTIFICATE = 11;
72 private const SERVER_KEY_EXCHANGE = 12;
73 private const CERTIFICATE_REQUEST = 13;
74 private const SERVER_HELLO_DONE = 14;
75 private const CERTIFICATE_VERIFY = 15;
76 private const CLIENT_KEY_EXCHANGE = 16;
77 private const FINISHED = 20;
78 private const KEY_UPDATE = 24;
79 private const MESSAGE_HASH = 254;
80
81 // 拡張
82 private const EXT_SERVER_NAME = 0;
83 private const EXT_SUPPORTED_GROUPS = 10;
84 private const EXT_EC_POINT_FORMATS = 11;
85 private const EXT_SIGNATURE_ALGORITHMS = 13;
86 private const EXT_ALPN = 16;
87 private const EXT_EXTENDED_MASTER_SECRET = 23;
88 private const EXT_SUPPORTED_VERSIONS = 43;
89 private const EXT_COOKIE = 44;
90 private const EXT_KEY_SHARE = 51;
91 private const EXT_RENEGOTIATION_INFO = 0xFF01;
92
93 // 暗号スイート => [AEAD, 鍵の長さ, ハッシュ, 名前]
94 public const SUITES = [
95 0x1303 => ['chacha', 32, 'sha256', 'TLS_CHACHA20_POLY1305_SHA256'],
96 0x1301 => ['aes', 16, 'sha256', 'TLS_AES_128_GCM_SHA256'],
97 0x1302 => ['aes', 32, 'sha384', 'TLS_AES_256_GCM_SHA384'],
98 ];
99
100 // TLS 1.2の暗号スイート(ECDHE+AEADだけ) => [AEAD, 鍵の長さ, PRFのハッシュ, 名前, 証明書の鍵]
101 public const SUITES12 = [
102 0xCCA9 => ['chacha', 32, 'sha256', 'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256', 'ec'],
103 0xCCA8 => ['chacha', 32, 'sha256', 'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256', 'rsa'],
104 0xC02B => ['aes', 16, 'sha256', 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256', 'ec'],
105 0xC02F => ['aes', 16, 'sha256', 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256', 'rsa'],
106 0xC02C => ['aes', 32, 'sha384', 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384', 'ec'],
107 0xC030 => ['aes', 32, 'sha384', 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', 'rsa'],
108 ];
109
110 public const VERSIONS = ['1.2' => 0x0303, '1.3' => 0x0304];
111
112 // 鍵交換のグループ
113 public const GROUPS = ['x25519' => 0x001D, 'P-256' => 0x0017, 'P-384' => 0x0018];
114
115 // 署名の方式 => [種類, ハッシュ, 曲線]
116 private const SIG_SCHEMES = [
117 0x0403 => ['ec', 'sha256', 'P-256'],
118 0x0503 => ['ec', 'sha384', 'P-384'],
119 0x0603 => ['ec', 'sha512', 'P-521'],
120 0x0804 => ['pss', 'sha256', null],
121 0x0805 => ['pss', 'sha384', null],
122 0x0806 => ['pss', 'sha512', null],
123 0x0401 => ['pkcs1', 'sha256', null],
124 0x0501 => ['pkcs1', 'sha384', null],
125 0x0601 => ['pkcs1', 'sha512', null],
126 ];
127
128 // アラート
129 public const ALERT_CLOSE_NOTIFY = 0;
130 public const ALERT_UNEXPECTED_MESSAGE = 10;
131 public const ALERT_BAD_RECORD_MAC = 20;
132 public const ALERT_RECORD_OVERFLOW = 22;
133 public const ALERT_HANDSHAKE_FAILURE = 40;
134 public const ALERT_BAD_CERTIFICATE = 42;
135 public const ALERT_CERTIFICATE_EXPIRED = 45;
136 public const ALERT_CERTIFICATE_UNKNOWN = 46;
137 public const ALERT_ILLEGAL_PARAMETER = 47;
138 public const ALERT_UNKNOWN_CA = 48;
139 public const ALERT_DECODE_ERROR = 50;
140 public const ALERT_DECRYPT_ERROR = 51;
141 public const ALERT_PROTOCOL_VERSION = 70;
142 public const ALERT_INTERNAL_ERROR = 80;
143 public const ALERT_USER_CANCELED = 90;
144 public const ALERT_NO_RENEGOTIATION = 100;
145 public const ALERT_MISSING_EXTENSION = 109;
146 public const ALERT_UNSUPPORTED_EXTENSION = 110;
147 public const ALERT_UNRECOGNIZED_NAME = 112;
148 public const ALERT_NO_APPLICATION_PROTOCOL = 120;
149
150 private const HRR_RANDOM = "\xCF\x21\xAD\x74\xE5\x9A\x61\x11\xBE\x1D\x8C\x02\x1E\x65\xB8\x91\xC2\xA2\x11\x16\x7A\xBB\x8C\x5E\x07\x9E\x09\xE2\xC8\xA8\x33\x9C";
151 private const MAX_PLAIN = 16384;
152 private const DOWNGRADE_12 = "DOWNGRD\x01";
153
154 /** @var resource */
155 private $sock;
156 private bool $server;
157 private array $opt;
158
159 // レコード層
160 private string $in = '';
161 private ?object $readAead = null;
162 private ?object $writeAead = null;
163 private string $readIv = '';
164 private string $writeIv = '';
165 private int $readSeq = 0;
166 private int $writeSeq = 0;
167 private string $readSecret = '';
168 private string $writeSecret = '';
169 private string $hs = '';
170 private string $app = '';
171 /** TLS 1.2:ChangeCipherSpecで使い始める鍵 */
172 private ?array $pendingRead = null;
173 private ?array $pendingWrite = null;
174
175 // 状態
176 private bool $established = false;
177 private bool $peerClosed = false;
178 private bool $closed = false;
179 private bool $rawEof = false;
180 private bool $timedOut = false;
181 private ?TlsException $error = null;
182
183 // ハンドシェイク
184 private int $version = 0x0304;
185 private int $minVersion;
186 private int $maxVersion;
187 private string $hash = 'sha256';
188 private string $transcript = '';
189 private int $suite = 0;
190 private string $group = '';
191 private string $alpn = '';
192 private array $peerCerts = [];
193 private string $sni = '';
194
195 /**
196 * @param resource $sock 接続済みのソケット
197 * @param array $opt オプション(Std\Lib\Tlsを参照)
198 * @param bool $server サーバー側かどうか
199 */
200 public function __construct($sock, array $opt, bool $server) {
201 $this->sock = $sock;
202 $this->opt = $opt;
203 $this->server = $server;
204 $this->minVersion = self::VERSIONS[(string)($opt['min_version'] ?? '1.2')] ?? throw new \InvalidArgumentException('TLS:min_versionは1.2か1.3');
205 $this->maxVersion = self::VERSIONS[(string)($opt['max_version'] ?? '1.3')] ?? throw new \InvalidArgumentException('TLS:max_versionは1.2か1.3');
206 if ($this->minVersion > $this->maxVersion) throw new \InvalidArgumentException('TLS:min_versionがmax_versionより大きい');
207 }
208
209 /** 下のソケット */
210 public function socket() {
211 return $this->sock;
212 }
213
214 /**
215 * ハンドシェイクを行う
216 *
217 * @return void
218 * @throws TlsException 失敗した場合(相手にもアラートを送る)
219 */
220 public function handshake(): void {
221 try {
222 $this->server ? $this->serverHandshake() : $this->clientHandshake();
223 $this->established = true;
224 } catch (TlsException $e) {
225 if (!$e->received) $this->sendAlert($e->alert);
226 $this->error = $e;
227 throw $e;
228 } catch (\Throwable $e) {
229 $this->sendAlert(self::ALERT_INTERNAL_ERROR);
230 throw $this->error = new TlsException('TLS:内部エラー:'.$e->getMessage(), self::ALERT_INTERNAL_ERROR, false, $e);
231 }
232 }
233
234 /**
235 * 接続の情報
236 *
237 * @return array version、cipher、group、alpn、peer_certificates(X509[])、server_name(サーバー側)
238 */
239 public function info(): array {
240 return [
241 'version' => $this->version === 0x0303 ? 'TLSv1.2' : 'TLSv1.3',
242 'cipher' => ($this->version === 0x0303 ? self::SUITES12 : self::SUITES)[$this->suite][3] ?? '',
243 'group' => $this->group,
244 'alpn' => $this->alpn,
245 'peer_certificates' => $this->peerCerts,
246 'server_name' => $this->sni,
247 ];
248 }
249
250 /**
251 * アプリケーションデータを読む
252 *
253 * @param int $len 最大のバイト数
254 * @return string データ、終わり又はタイムアウトの場合は空
255 * @throws TlsException
256 */
257 public function read(int $len): string {
258 while ($this->app === '' && !$this->eof()) {
259 if (!$this->readRecordInto()) break;
260 }
261 $out = substr($this->app, 0, $len);
262 $this->app = (string)substr($this->app, strlen($out));
263 return $out;
264 }
265
266 /**
267 * アプリケーションデータを書く
268 *
269 * @param string $data
270 * @return int 書いたバイト数
271 * @throws TlsException
272 */
273 public function write(string $data): int {
274 if ($this->closed || !$this->established) throw new TlsException('TLS:接続が閉じています', self::ALERT_INTERNAL_ERROR);
275 $this->writeRecord(self::APPDATA, $data);
276 return strlen($data);
277 }
278
279 /** データの終わりかどうか */
280 public function eof(): bool {
281 return $this->app === '' && ($this->peerClosed || $this->rawEof || $this->error !== null);
282 }
283
284 /** 復号済み又は受信済みのデータが残っているかどうか(stream_selectでは分からない) */
285 public function pending(): bool {
286 return $this->app !== '' || $this->in !== '';
287 }
288
289 /** 最後の読み込みがタイムアウトしたかどうか */
290 public function timedOut(): bool {
291 return $this->timedOut;
292 }
293
294 /** 読み込み中のエラー(ストリームでは例外を投げられないので) */
295 public function lastError(): ?TlsException {
296 return $this->error;
297 }
298
299 /**
300 * close_notifyを送って閉じる(下のソケットも閉じる)
301 *
302 * @return void
303 */
304 public function close(): void {
305 if ($this->closed) return;
306 if ($this->established && $this->error === null) $this->sendAlert(self::ALERT_CLOSE_NOTIFY, 1);
307 $this->closed = true;
308 if (is_resource($this->sock)) @fclose($this->sock);
309 }
310
311 ///////////////
312 // クライアント
313
314 private function clientHandshake(): void {
315 $host = (string)($this->opt['peer_name'] ?? '');
316 $v13 = $this->maxVersion >= 0x0304;
317 $v12 = $this->minVersion <= 0x0303;
318 $s13 = $v13 ? $this->allowedSuites(self::SUITES) : [];
319 $s12 = $v12 ? $this->allowedSuites(self::SUITES12) : [];
320 if (empty($s13) && empty($s12)) throw new TlsException('TLS:暗号スイートがありません', self::ALERT_INTERNAL_ERROR);
321 $suites = array_merge($s13, $s12);
322 $groups = $this->allowedGroups();
323 // _random・_session_id・_keysはテスト用(記録したハンドシェイクを再現する)
324 $random = $this->opt['_random'] ?? random_bytes(32);
325 $sid = $this->opt['_session_id'] ?? random_bytes(32);
326
327 $first = $groups[0];
328 $keys = $v13 ? [$first => $this->newShare($first)] : [];
329 $cookie = '';
330 $hello = $this->clientHello($random, $sid, $suites, $groups, $keys, $host, $cookie, $v13, $v12);
331 $this->writeRecord(self::HANDSHAKE, $hello, 0x0301);
332 $this->transcript = $hello;
333
334 [$type, $sh] = $this->readHandshake();
335 if ($type !== self::SERVER_HELLO) throw new TlsException('TLS:ServerHelloを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
336 $s = $this->parseServerHello($sh, $v13, $v12);
337
338 if ($s['version'] === 0x0303) {
339 if (!in_array($s['suite'], $s12, true)) throw new TlsException('TLS:不正なServerHello(暗号スイート)', self::ALERT_ILLEGAL_PARAMETER);
340 $this->clientHandshake12($s, $sh, $random, $host, $groups);
341 return;
342 }
343 if ($s['echo'] !== $sid || !in_array($s['suite'], $s13, true)) throw new TlsException('TLS:不正なServerHello', self::ALERT_ILLEGAL_PARAMETER);
344
345 if ($s['random'] === self::HRR_RANDOM) {
346 // HelloRetryRequest:別のグループで鍵を送り直す
347 $g = $s['hrr_group'];
348 if ($g === null || !in_array($g, $groups, true) || isset($keys[$g])) throw new TlsException('TLS:不正なHelloRetryRequest', self::ALERT_ILLEGAL_PARAMETER);
349 $this->hash = self::SUITES[$s['suite']][2];
350 $this->transcript = $this->messageHash($hello).$this->msg(self::SERVER_HELLO, $sh);
351 $keys = [$g => $this->newShare($g)];
352 $cookie = $s['cookie'];
353 // 2回目のClientHelloは鍵とcookie以外を変えない(RFC 8446 4.1.2)
354 $hello = $this->clientHello($random, $sid, $suites, $groups, $keys, $host, $cookie, $v13, $v12);
355 $this->writeCcs();
356 $ccsSent = true;
357 $this->writeRecord(self::HANDSHAKE, $hello);
358 $this->transcript .= $hello;
359 [$type, $sh] = $this->readHandshake();
360 if ($type !== self::SERVER_HELLO) throw new TlsException('TLS:ServerHelloを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
361 $s2 = $this->parseServerHello($sh, true, false);
362 if ($s2['random'] === self::HRR_RANDOM) throw new TlsException('TLS:2回目のHelloRetryRequest', self::ALERT_UNEXPECTED_MESSAGE);
363 if ($s2['echo'] !== $sid || $s2['suite'] !== $s['suite']) throw new TlsException('TLS:不正なServerHello', self::ALERT_ILLEGAL_PARAMETER);
364 $s = $s2;
365 }
366
367 if ($s['share'] === null || !isset($keys[$s['share'][0]])) throw new TlsException('TLS:サーバーの鍵が不正です', self::ALERT_ILLEGAL_PARAMETER);
368 $this->suite = $s['suite'];
369 $this->hash = self::SUITES[$this->suite][2];
370 $this->transcript .= $this->msg(self::SERVER_HELLO, $sh);
371 $this->group = (string)array_search($s['share'][0], self::GROUPS, true);
372 $shared = $this->sharedSecret($keys[$s['share'][0]], $s['share'][0], $s['share'][1]);
373
374 [$cHs, $sHs, $master] = $this->handshakeSecrets($shared);
375 $this->setReadKey($sHs);
376
377 // EncryptedExtensions
378 [$type, $body] = $this->readHandshake();
379 if ($type !== self::ENCRYPTED_EXTENSIONS) throw new TlsException('TLS:EncryptedExtensionsを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
380 $this->transcript .= $this->msg($type, $body);
381 $ee = $this->parseExtensions($this->vec($body, 0, 2, $end));
382 if ($end !== strlen($body)) throw new TlsException('TLS:不正なEncryptedExtensions', self::ALERT_DECODE_ERROR);
383 if (isset($ee[self::EXT_ALPN])) $this->alpn = $this->alpnReply($ee[self::EXT_ALPN]);
384
385 // CertificateRequest(任意)、Certificate、CertificateVerify
386 [$type, $body] = $this->readHandshake();
387 $certRequest = null;
388 if ($type === self::CERTIFICATE_REQUEST) {
389 $this->transcript .= $this->msg($type, $body);
390 $certRequest = $this->vec($body, 0, 1);
391 [$type, $body] = $this->readHandshake();
392 }
393 if ($type !== self::CERTIFICATE) throw new TlsException('TLS:Certificateを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
394 $this->transcript .= $this->msg($type, $body);
395 $this->peerCerts = $this->parseCertificate($body);
396 if (empty($this->peerCerts)) throw new TlsException('TLS:サーバーの証明書がありません', self::ALERT_DECODE_ERROR);
397
398 [$type, $body] = $this->readHandshake();
399 if ($type !== self::CERTIFICATE_VERIFY) throw new TlsException('TLS:CertificateVerifyを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
400 $this->checkCertificateVerify($body, $this->peerCerts[0], 'TLS 1.3, server CertificateVerify');
401 $this->transcript .= $this->msg($type, $body);
402 $this->verifyPeer($host);
403
404 // サーバーのFinished
405 [$type, $body] = $this->readHandshake();
406 if ($type !== self::FINISHED) throw new TlsException('TLS:Finishedを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
407 if (!hash_equals($this->finished($sHs), $body)) throw new TlsException('TLS:サーバーのFinishedが不正です', self::ALERT_DECRYPT_ERROR);
408 $this->transcript .= $this->msg($type, $body);
409 [$cAp, $sAp] = $this->appSecrets($master);
410 if ($this->hs !== '') throw new TlsException('TLS:鍵の変更の前に余計なデータ', self::ALERT_UNEXPECTED_MESSAGE);
411
412 // クライアントのFinished(互換性のためのChangeCipherSpecは1回だけ)
413 if (empty($ccsSent)) $this->writeCcs();
414 $this->setWriteKey($cHs);
415 if ($certRequest !== null) {
416 $cert = $this->msg(self::CERTIFICATE, $this->vec2($certRequest, 1)."\x00\x00\x00");
417 $this->transcript .= $cert;
418 $this->writeRecord(self::HANDSHAKE, $cert);
419 }
420 $fin = $this->msg(self::FINISHED, $this->finished($cHs));
421 $this->writeRecord(self::HANDSHAKE, $fin);
422 $this->transcript .= $fin;
423
424 $this->setReadKey($sAp);
425 $this->setWriteKey($cAp);
426 }
427
428 /** 使う暗号スイート(ciphersで絞る) */
429 private function allowedSuites(array $table): array {
430 $only = $this->opt['ciphers'] ?? null;
431 return array_values(array_filter(array_keys($table), fn(int $id): bool => $only === null || in_array($id, $only, true)));
432 }
433
434 /** 使う鍵交換のグループ(groupsの順) */
435 private function allowedGroups(): array {
436 $out = [];
437 foreach ($this->opt['groups'] ?? array_keys(self::GROUPS) as $g) {
438 if (isset(self::GROUPS[$g])) $out[] = self::GROUPS[$g];
439 }
440 if (empty($out)) throw new TlsException('TLS:鍵交換のグループがありません', self::ALERT_INTERNAL_ERROR);
441 return $out;
442 }
443
444 private function clientHello(string $random, string $sid, array $suites, array $groups, array $keys, string $host, string $cookie, bool $v13, bool $v12): string {
445 $ext = '';
446 if ($host !== '' && ($this->opt['SNI_enabled'] ?? true) && @inet_pton(trim($host, '[]')) === false) {
447 $name = "\x00".$this->vec2($host, 2);
448 $ext .= $this->ext(self::EXT_SERVER_NAME, $this->vec2($name, 2));
449 }
450 $ext .= $this->ext(self::EXT_SUPPORTED_GROUPS, $this->vec2(pack('n*', ...$groups), 2));
451 $ext .= $this->ext(self::EXT_SIGNATURE_ALGORITHMS, $this->vec2(pack('n*', ...array_keys(self::SIG_SCHEMES)), 2));
452 if ($v13) {
453 $ext .= $this->ext(self::EXT_SUPPORTED_VERSIONS, $this->vec2($v12 ? "\x03\x04\x03\x03" : "\x03\x04", 1));
454 $shares = '';
455 foreach ($keys as $g => $k) $shares .= pack('n', $g).$this->vec2($k['pub'], 2);
456 $ext .= $this->ext(self::EXT_KEY_SHARE, $this->vec2($shares, 2));
457 }
458 if ($v12) {
459 // TLS 1.2用:圧縮しない点、Extended Master Secret、安全な再ネゴシエーションの印(再ネゴシエーションはしない)
460 $ext .= $this->ext(self::EXT_EC_POINT_FORMATS, "\x01\x00");
461 $ext .= $this->ext(self::EXT_EXTENDED_MASTER_SECRET, '');
462 $ext .= $this->ext(self::EXT_RENEGOTIATION_INFO, "\x00");
463 }
464 if (!empty($this->opt['alpn'])) {
465 $list = '';
466 foreach ($this->opt['alpn'] as $p) $list .= $this->vec2($p, 1);
467 $ext .= $this->ext(self::EXT_ALPN, $this->vec2($list, 2));
468 }
469 if ($cookie !== '') $ext .= $this->ext(self::EXT_COOKIE, $cookie);
470
471 $body = "\x03\x03".$random.$this->vec2($sid, 1).$this->vec2(pack('n*', ...$suites), 2)."\x01\x00".$this->vec2($ext, 2);
472 return $this->msg(self::CLIENT_HELLO, $body);
473 }
474
475 private function parseServerHello(string $b, bool $v13, bool $v12): array {
476 $pos = 2;
477 if (substr($b, 0, 2) !== "\x03\x03") throw new TlsException('TLS:サーバーがTLS 1.2以上に対応していません', self::ALERT_PROTOCOL_VERSION);
478 $random = substr($b, $pos, 32);
479 $pos += 32;
480 $echo = $this->vec($b, $pos, 1, $pos);
481 $suite = $this->u16($b, $pos);
482 $pos += 2;
483 $comp = ord($b[$pos] ?? "\1");
484 $pos++;
485 $ext = $this->parseExtensions($this->vec($b, $pos, 2, $pos));
486 if ($pos !== strlen($b) || strlen($random) !== 32) throw new TlsException('TLS:不正なServerHello', self::ALERT_DECODE_ERROR);
487
488 if ($comp !== 0) throw new TlsException('TLS:不正なServerHello(圧縮)', self::ALERT_ILLEGAL_PARAMETER);
489 // TLS 1.3の印(supported_versions)が無ければTLS 1.2
490 if (isset($ext[self::EXT_SUPPORTED_VERSIONS])) {
491 if ($ext[self::EXT_SUPPORTED_VERSIONS] !== "\x03\x04" || !$v13) throw new TlsException('TLS:不正なバージョン', self::ALERT_ILLEGAL_PARAMETER);
492 $version = 0x0304;
493 } else {
494 if (!$v12) throw new TlsException('TLS:サーバーがTLS 1.3に対応していません', self::ALERT_PROTOCOL_VERSION);
495 $version = 0x0303;
496 if (isset($ext[self::EXT_KEY_SHARE]) || $random === self::HRR_RANDOM) throw new TlsException('TLS:不正なServerHello', self::ALERT_ILLEGAL_PARAMETER);
497 }
498 // TLS 1.2以下に下げる攻撃の印(RFC 8446 4.1.3):TLS 1.3を送ったのに1.2になった
499 $tail = substr($random, -8);
500 if (($version === 0x0303 && $v13 && $tail === self::DOWNGRADE_12) || $tail === "DOWNGRD\x00") {
501 throw new TlsException('TLS:ダウングレードの攻撃(サーバーはTLS 1.3に対応している)', self::ALERT_ILLEGAL_PARAMETER);
502 }
503
504 $res = ['version' => $version, 'random' => $random, 'echo' => $echo, 'suite' => $suite, 'ext' => $ext, 'share' => null, 'hrr_group' => null, 'cookie' => ''];
505 if ($version === 0x0303) return $res;
506 if (isset($ext[self::EXT_KEY_SHARE])) {
507 $k = $ext[self::EXT_KEY_SHARE];
508 if ($random === self::HRR_RANDOM) {
509 if (strlen($k) !== 2) throw new TlsException('TLS:不正なHelloRetryRequest', self::ALERT_DECODE_ERROR);
510 $res['hrr_group'] = $this->u16($k, 0);
511 } else {
512 $p = 2;
513 $res['share'] = [$this->u16($k, 0), $this->vec($k, $p, 2, $p)];
514 if ($p !== strlen($k)) throw new TlsException('TLS:不正なkey_share', self::ALERT_DECODE_ERROR);
515 }
516 } else if ($random !== self::HRR_RANDOM) {
517 throw new TlsException('TLS:key_shareがありません', self::ALERT_MISSING_EXTENSION);
518 }
519 if (isset($ext[self::EXT_COOKIE])) $res['cookie'] = $ext[self::EXT_COOKIE];
520 return $res;
521 }
522
523 /**
524 * TLS 1.2のハンドシェイク(クライアント、ServerHelloの後)
525 *
526 * Certificate、ServerKeyExchange(ECDHE、署名を確認する)、ServerHelloDone、
527 * ClientKeyExchange、ChangeCipherSpec、Finished、相手のChangeCipherSpec・Finished
528 */
529 private function clientHandshake12(array $s, string $sh, string $random, string $host, array $groups): void {
530 $this->version = 0x0303;
531 $this->suite = $s['suite'];
532 $this->hash = self::SUITES12[$this->suite][2];
533 $this->transcript .= $this->msg(self::SERVER_HELLO, $sh);
534
535 // 送っていない拡張は拒否する
536 $ext = $s['ext'];
537 foreach (array_keys($ext) as $t) {
538 if (!in_array($t, [self::EXT_SERVER_NAME, self::EXT_EC_POINT_FORMATS, self::EXT_ALPN, self::EXT_EXTENDED_MASTER_SECRET, self::EXT_RENEGOTIATION_INFO], true)) {
539 throw new TlsException("TLS:送っていない拡張:{$t}", self::ALERT_UNSUPPORTED_EXTENSION);
540 }
541 }
542 if (isset($ext[self::EXT_RENEGOTIATION_INFO]) && $ext[self::EXT_RENEGOTIATION_INFO] !== "\x00") {
543 throw new TlsException('TLS:不正なrenegotiation_info', self::ALERT_HANDSHAKE_FAILURE);
544 }
545 if (isset($ext[self::EXT_EC_POINT_FORMATS]) && !str_contains($this->vec($ext[self::EXT_EC_POINT_FORMATS], 0, 1), "\x00")) {
546 throw new TlsException('TLS:圧縮しない点に対応していません', self::ALERT_ILLEGAL_PARAMETER);
547 }
548 $ems = isset($ext[self::EXT_EXTENDED_MASTER_SECRET]);
549 if ($ems && $ext[self::EXT_EXTENDED_MASTER_SECRET] !== '') throw new TlsException('TLS:不正なextended_master_secret', self::ALERT_DECODE_ERROR);
550 if (isset($ext[self::EXT_ALPN])) $this->alpn = $this->alpnReply($ext[self::EXT_ALPN]);
551
552 // Certificate
553 [$type, $body] = $this->readHandshake();
554 if ($type !== self::CERTIFICATE) throw new TlsException('TLS:Certificateを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
555 $this->transcript .= $this->msg($type, $body);
556 $this->peerCerts = $this->parseCertificate12($body);
557 $key = $this->peerKey();
558 if ((self::SUITES12[$this->suite][4] === 'ec') !== ($key instanceof Ec)) {
559 throw new TlsException('TLS:証明書の鍵が暗号スイートと合いません', self::ALERT_BAD_CERTIFICATE);
560 }
561
562 // ServerKeyExchange(名前付き曲線のECDHE)
563 [$type, $body] = $this->readHandshake();
564 if ($type !== self::SERVER_KEY_EXCHANGE) throw new TlsException('TLS:ServerKeyExchangeを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
565 if (($body[0] ?? '') !== "\x03") throw new TlsException('TLS:名前付き曲線ではありません', self::ALERT_ILLEGAL_PARAMETER);
566 $group = $this->u16($body, 1);
567 $point = $this->vec($body, 3, 1, $p);
568 $params = substr($body, 0, $p);
569 $scheme = $this->u16($body, $p);
570 $sig = $this->vec($body, $p + 2, 2, $end);
571 if ($end !== strlen($body)) throw new TlsException('TLS:不正なServerKeyExchange', self::ALERT_DECODE_ERROR);
572 if (!in_array($group, $groups, true)) throw new TlsException('TLS:送っていない鍵交換のグループ', self::ALERT_ILLEGAL_PARAMETER);
573 if (!$this->verify12($key, $scheme, $random.$s['random'].$params, $sig)) {
574 throw new TlsException('TLS:ServerKeyExchangeの署名が不正です', self::ALERT_DECRYPT_ERROR);
575 }
576 $this->transcript .= $this->msg($type, $body);
577 $this->verifyPeer($host);
578
579 // CertificateRequest(任意)、ServerHelloDone
580 [$type, $body] = $this->readHandshake();
581 $certRequest = false;
582 if ($type === self::CERTIFICATE_REQUEST) {
583 $this->transcript .= $this->msg($type, $body);
584 $certRequest = true;
585 [$type, $body] = $this->readHandshake();
586 }
587 if ($type !== self::SERVER_HELLO_DONE || $body !== '') throw new TlsException('TLS:ServerHelloDoneを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
588 $this->transcript .= $this->msg($type, $body);
589
590 // ClientKeyExchange(求められたら空の証明書)
591 $this->group = (string)array_search($group, self::GROUPS, true);
592 $mine = $this->newShare($group);
593 $pms = $this->sharedSecret($mine, $group, $point);
594 $out = '';
595 if ($certRequest) {
596 $out .= $m = $this->msg(self::CERTIFICATE, "\x00\x00\x00");
597 $this->transcript .= $m;
598 }
599 $out .= $m = $this->msg(self::CLIENT_KEY_EXCHANGE, $this->vec2($mine['pub'], 1));
600 $this->transcript .= $m;
601 $master = $this->masterSecret($pms, $random, $s['random'], $ems);
602 $this->keys12($master, $random, $s['random']);
603 $this->writeRecord(self::HANDSHAKE, $out);
604
605 // ChangeCipherSpecとFinished
606 $this->writeCcs();
607 $this->activateWrite12();
608 $fin = $this->msg(self::FINISHED, $this->finished12($master, 'client finished'));
609 $this->writeRecord(self::HANDSHAKE, $fin);
610 $this->transcript .= $fin;
611
612 // サーバーのChangeCipherSpec(readRecordで鍵を変える)とFinished
613 [$type, $body] = $this->readHandshake();
614 if ($this->readAead === null || $type !== self::FINISHED) throw new TlsException('TLS:Finishedを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
615 if (!hash_equals($this->finished12($master, 'server finished'), $body)) throw new TlsException('TLS:サーバーのFinishedが不正です', self::ALERT_DECRYPT_ERROR);
616 $this->transcript .= $this->msg($type, $body);
617 if ($this->hs !== '') throw new TlsException('TLS:Finishedの後に余計なデータ', self::ALERT_UNEXPECTED_MESSAGE);
618 }
619
620 /** ALPNの返事(送った物の1つ) */
621 private function alpnReply(string $data): string {
622 $list = $this->vec($data, 0, 2);
623 $p = $this->vec($list, 0, 1, $end);
624 if ($end !== strlen($list) || !in_array($p, $this->opt['alpn'] ?? [], true)) throw new TlsException('TLS:不正なALPN', self::ALERT_ILLEGAL_PARAMETER);
625 return $p;
626 }
627
628 /** サーバーの証明書の公開鍵 */
629 private function peerKey(): Rsa|Ec {
630 if (empty($this->peerCerts)) throw new TlsException('TLS:サーバーの証明書がありません', self::ALERT_DECODE_ERROR);
631 try {
632 return $this->peerCerts[0]->publicKey();
633 } catch (\Throwable $e) {
634 throw new TlsException('TLS:未対応の証明書の鍵', self::ALERT_BAD_CERTIFICATE, false, $e);
635 }
636 }
637
638 /** 証明書の確認(verify_peer) */
639 private function verifyPeer(string $host): void {
640 if (!($this->opt['verify_peer'] ?? true)) return;
641 $name = ($this->opt['verify_peer_name'] ?? true) ? $host : null;
642 if ($name === '') throw new TlsException('TLS:peer_nameがありません(証明書を確認出来ません)', self::ALERT_INTERNAL_ERROR);
643 try {
644 X509::verifyChain($this->peerCerts, $name, [
645 'cafile' => $this->opt['cafile'] ?? null,
646 'allow_self_signed' => (bool)($this->opt['allow_self_signed'] ?? false),
647 'time' => $this->opt['_time'] ?? null,
648 ]);
649 } catch (\RuntimeException $e) {
650 $alert = str_contains($e->getMessage(), '期限') ? self::ALERT_CERTIFICATE_EXPIRED
651 : (str_contains($e->getMessage(), '信頼') || str_contains($e->getMessage(), '自己署名') ? self::ALERT_UNKNOWN_CA : self::ALERT_BAD_CERTIFICATE);
652 throw new TlsException('TLS:証明書の確認に失敗:'.$e->getMessage(), $alert, false, $e);
653 }
654 }
655
656 ///////////////
657 // サーバー
658
659 private function serverHandshake(): void {
660 [$certs, $key] = $this->serverIdentity();
661 $groups = $this->allowedGroups();
662
663 [$type, $body] = $this->readHandshake();
664 if ($type !== self::CLIENT_HELLO) throw new TlsException('TLS:ClientHelloを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
665 $ch = $this->parseClientHello($body);
666 $this->transcript = $this->msg($type, $body);
667 $this->sni = $ch['sni'];
668
669 // バージョン:両方が対応している一番新しい物
670 $offers13 = in_array(0x0304, $ch['versions'], true);
671 $offers12 = in_array(0x0303, $ch['versions'], true) || (empty($ch['versions']) && $ch['legacy'] >= 0x0303);
672 if (!($offers13 && $this->maxVersion >= 0x0304)) {
673 if ($offers12 && $this->minVersion <= 0x0303) {
674 $this->serverHandshake12($ch, $certs, $key, $groups);
675 return;
676 }
677 throw new TlsException('TLS:クライアントが対応しているバージョンがありません', self::ALERT_PROTOCOL_VERSION);
678 }
679 if ($ch['sigs'] === null || $ch['groups'] === null || $ch['shares'] === null) {
680 throw new TlsException('TLS:必要な拡張がありません', self::ALERT_MISSING_EXTENSION);
681 }
682 $suites = $this->allowedSuites(self::SUITES);
683
684 $suite = null;
685 foreach ($ch['suites'] as $s) {
686 if (in_array($s, $suites, true)) {
687 $suite = $s;
688 break;
689 }
690 }
691 if ($suite === null) throw new TlsException('TLS:共通の暗号スイートがありません', self::ALERT_HANDSHAKE_FAILURE);
692 $this->suite = $suite;
693 $this->hash = self::SUITES[$suite][2];
694 $scheme = $this->chooseScheme($key, $ch['sigs']);
695
696 // 鍵交換:クライアントが送った鍵、無ければHelloRetryRequest
697 $share = null;
698 foreach ($groups as $g) {
699 if (isset($ch['shares'][$g])) {
700 $share = [$g, $ch['shares'][$g]];
701 break;
702 }
703 }
704 if ($share === null) {
705 $g = null;
706 foreach ($groups as $x) {
707 if (in_array($x, $ch['groups'], true)) {
708 $g = $x;
709 break;
710 }
711 }
712 if ($g === null) throw new TlsException('TLS:共通の鍵交換のグループがありません', self::ALERT_HANDSHAKE_FAILURE);
713 $hrr = $this->msg(self::SERVER_HELLO, "\x03\x03".self::HRR_RANDOM.$this->vec2($ch['sid'], 1).pack('n', $suite)."\x00"
714 .$this->vec2($this->ext(self::EXT_SUPPORTED_VERSIONS, "\x03\x04").$this->ext(self::EXT_KEY_SHARE, pack('n', $g)), 2));
715 $this->transcript = $this->messageHash($this->transcript).$hrr;
716 $this->writeRecord(self::HANDSHAKE, $hrr);
717 if ($ch['sid'] !== '') $this->writeCcs();
718
719 [$type, $body] = $this->readHandshake();
720 if ($type !== self::CLIENT_HELLO) throw new TlsException('TLS:ClientHelloを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
721 $ch2 = $this->parseClientHello($body);
722 if (!isset($ch2['shares'][$g]) || count($ch2['shares']) !== 1 || !in_array($suite, $ch2['suites'], true) || !in_array(0x0304, $ch2['versions'], true)) {
723 throw new TlsException('TLS:2回目のClientHelloが不正です', self::ALERT_ILLEGAL_PARAMETER);
724 }
725 $this->transcript .= $this->msg($type, $body);
726 $share = [$g, $ch2['shares'][$g]];
727 $ch['alpn'] = $ch2['alpn'];
728 $hrrSent = true;
729 }
730
731 $this->group = (string)array_search($share[0], self::GROUPS, true);
732 $mine = $this->newShare($share[0]);
733 $shared = $this->sharedSecret($mine, $share[0], $share[1]);
734
735 $this->chooseAlpn($ch['alpn']);
736
737 $sh = $this->msg(self::SERVER_HELLO, "\x03\x03".random_bytes(32).$this->vec2($ch['sid'], 1).pack('n', $suite)."\x00"
738 .$this->vec2($this->ext(self::EXT_SUPPORTED_VERSIONS, "\x03\x04").$this->ext(self::EXT_KEY_SHARE, pack('n', $share[0]).$this->vec2($mine['pub'], 2)), 2));
739 $this->writeRecord(self::HANDSHAKE, $sh);
740 $this->transcript .= $sh;
741 if ($ch['sid'] !== '' && empty($hrrSent)) $this->writeCcs();
742
743 [$cHs, $sHs, $master] = $this->handshakeSecrets($shared);
744 $this->setWriteKey($sHs);
745
746 $out = '';
747 $ee = $this->alpn !== '' ? $this->ext(self::EXT_ALPN, $this->vec2($this->vec2($this->alpn, 1), 2)) : '';
748 $out .= $m = $this->msg(self::ENCRYPTED_EXTENSIONS, $this->vec2($ee, 2));
749 $this->transcript .= $m;
750
751 $list = '';
752 foreach ($certs as $c) $list .= $this->vec2($c->der, 3)."\x00\x00";
753 $out .= $m = $this->msg(self::CERTIFICATE, "\x00".$this->vec2($list, 3));
754 $this->transcript .= $m;
755
756 $out .= $m = $this->msg(self::CERTIFICATE_VERIFY, pack('n', $scheme).$this->vec2($this->signTranscript($key, $scheme, 'TLS 1.3, server CertificateVerify'), 2));
757 $this->transcript .= $m;
758
759 $out .= $m = $this->msg(self::FINISHED, $this->finished($sHs));
760 $this->transcript .= $m;
761 $this->writeRecord(self::HANDSHAKE, $out);
762
763 [$cAp, $sAp] = $this->appSecrets($master);
764 $this->setWriteKey($sAp);
765 $this->setReadKey($cHs);
766
767 [$type, $body] = $this->readHandshake();
768 if ($type === self::CERTIFICATE) throw new TlsException('TLS:クライアント証明書を求めていません', self::ALERT_UNEXPECTED_MESSAGE);
769 if ($type !== self::FINISHED) throw new TlsException('TLS:Finishedを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
770 if (!hash_equals($this->finished($cHs), $body)) throw new TlsException('TLS:クライアントのFinishedが不正です', self::ALERT_DECRYPT_ERROR);
771 $this->transcript .= $this->msg($type, $body);
772 if ($this->hs !== '') throw new TlsException('TLS:鍵の変更の前に余計なデータ', self::ALERT_UNEXPECTED_MESSAGE);
773 $this->setReadKey($cAp);
774 }
775
776 /**
777 * TLS 1.2のハンドシェイク(サーバー、ClientHelloの後)
778 */
779 private function serverHandshake12(array $ch, array $certs, Rsa|Ec $key, array $groups): void {
780 $this->version = 0x0303;
781 $keyType = $key instanceof Rsa ? 'rsa' : 'ec';
782 $allowed = $this->allowedSuites(self::SUITES12);
783 $suite = null;
784 foreach ($ch['suites'] as $id) {
785 if (in_array($id, $allowed, true) && self::SUITES12[$id][4] === $keyType) {
786 $suite = $id;
787 break;
788 }
789 }
790 if ($suite === null) throw new TlsException('TLS:共通の暗号スイートがありません', self::ALERT_HANDSHAKE_FAILURE);
791 $this->suite = $suite;
792 $this->hash = self::SUITES12[$suite][2];
793
794 // 鍵交換のグループ(supported_groupsが無ければsecp256r1)
795 $group = null;
796 foreach ($ch['groups'] ?? [self::GROUPS['P-256']] as $g) {
797 if (in_array($g, $groups, true)) {
798 $group = $g;
799 break;
800 }
801 }
802 if ($group === null) throw new TlsException('TLS:共通の鍵交換のグループがありません', self::ALERT_HANDSHAKE_FAILURE);
803 $this->group = (string)array_search($group, self::GROUPS, true);
804 if (isset($ch['ext'][self::EXT_EC_POINT_FORMATS]) && !str_contains($this->vec($ch['ext'][self::EXT_EC_POINT_FORMATS], 0, 1), "\x00")) {
805 throw new TlsException('TLS:圧縮しない点に対応していません', self::ALERT_ILLEGAL_PARAMETER);
806 }
807 // signature_algorithmsが無い(SHA-1を意味する)クライアントは拒否する
808 if ($ch['sigs'] === null) throw new TlsException('TLS:signature_algorithmsがありません', self::ALERT_HANDSHAKE_FAILURE);
809 $scheme = $this->chooseScheme12($key, $ch['sigs']);
810 $ems = isset($ch['ext'][self::EXT_EXTENDED_MASTER_SECRET]);
811 $this->chooseAlpn($ch['alpn']);
812
813 // ServerHello(TLS 1.3に対応しているなら、ダウングレードの印を入れる)
814 $sr = random_bytes(24).($this->maxVersion >= 0x0304 ? self::DOWNGRADE_12 : random_bytes(8));
815 $ext = '';
816 if (isset($ch['ext'][self::EXT_RENEGOTIATION_INFO])) {
817 if ($ch['ext'][self::EXT_RENEGOTIATION_INFO] !== "\x00") throw new TlsException('TLS:不正なrenegotiation_info', self::ALERT_HANDSHAKE_FAILURE);
818 $ext .= $this->ext(self::EXT_RENEGOTIATION_INFO, "\x00");
819 } else if (in_array(0x00FF, $ch['suites'], true)) {
820 $ext .= $this->ext(self::EXT_RENEGOTIATION_INFO, "\x00"); // TLS_EMPTY_RENEGOTIATION_INFO_SCSV
821 }
822 if ($ems) $ext .= $this->ext(self::EXT_EXTENDED_MASTER_SECRET, '');
823 $ext .= $this->ext(self::EXT_EC_POINT_FORMATS, "\x01\x00");
824 if ($this->alpn !== '') $ext .= $this->ext(self::EXT_ALPN, $this->vec2($this->vec2($this->alpn, 1), 2));
825 $out = $m = $this->msg(self::SERVER_HELLO, "\x03\x03".$sr."\x00".pack('n', $suite)."\x00".$this->vec2($ext, 2));
826 $this->transcript .= $m;
827
828 $list = '';
829 foreach ($certs as $c) $list .= $this->vec2($c->der, 3);
830 $out .= $m = $this->msg(self::CERTIFICATE, $this->vec2($list, 3));
831 $this->transcript .= $m;
832
833 $mine = $this->newShare($group);
834 $params = "\x03".pack('n', $group).$this->vec2($mine['pub'], 1);
835 $out .= $m = $this->msg(self::SERVER_KEY_EXCHANGE, $params.pack('n', $scheme).$this->vec2($this->signKeyExchange($key, $scheme, $ch['random'].$sr.$params), 2));
836 $this->transcript .= $m;
837 $out .= $m = $this->msg(self::SERVER_HELLO_DONE, '');
838 $this->transcript .= $m;
839 $this->writeRecord(self::HANDSHAKE, $out);
840
841 // ClientKeyExchange
842 [$type, $body] = $this->readHandshake();
843 if ($type !== self::CLIENT_KEY_EXCHANGE) throw new TlsException('TLS:ClientKeyExchangeを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
844 $point = $this->vec($body, 0, 1, $end);
845 if ($end !== strlen($body)) throw new TlsException('TLS:不正なClientKeyExchange', self::ALERT_DECODE_ERROR);
846 $pms = $this->sharedSecret($mine, $group, $point);
847 $this->transcript .= $this->msg($type, $body);
848 $master = $this->masterSecret($pms, $ch['random'], $sr, $ems);
849 $this->keys12($master, $ch['random'], $sr);
850
851 // クライアントのChangeCipherSpecとFinished
852 [$type, $body] = $this->readHandshake();
853 if ($this->readAead === null || $type !== self::FINISHED) throw new TlsException('TLS:Finishedを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
854 if (!hash_equals($this->finished12($master, 'client finished'), $body)) throw new TlsException('TLS:クライアントのFinishedが不正です', self::ALERT_DECRYPT_ERROR);
855 $this->transcript .= $this->msg($type, $body);
856 if ($this->hs !== '') throw new TlsException('TLS:Finishedの後に余計なデータ', self::ALERT_UNEXPECTED_MESSAGE);
857
858 $this->writeCcs();
859 $this->activateWrite12();
860 $fin = $this->msg(self::FINISHED, $this->finished12($master, 'server finished'));
861 $this->writeRecord(self::HANDSHAKE, $fin);
862 $this->transcript .= $fin;
863 }
864
865 /** ALPN(サーバー):自分の順で、クライアントも対応している物 */
866 private function chooseAlpn(array $offered): void {
867 if (empty($offered) || empty($this->opt['alpn'])) return;
868 foreach ($this->opt['alpn'] as $p) {
869 if (in_array($p, $offered, true)) {
870 $this->alpn = $p;
871 return;
872 }
873 }
874 throw new TlsException('TLS:共通のアプリケーションプロトコルがありません', self::ALERT_NO_APPLICATION_PROTOCOL);
875 }
876
877 /** サーバーの証明書と秘密鍵(cert・key、又はlocal_cert・local_pkのPEM又はファイル) */
878 private function serverIdentity(): array {
879 $load = function(string $v): string {
880 return str_contains($v, '-----BEGIN') ? $v : (string)@file_get_contents($v);
881 };
882 $certPem = $load((string)($this->opt['cert'] ?? $this->opt['local_cert'] ?? ''));
883 $keyPem = $load((string)($this->opt['key'] ?? $this->opt['local_pk'] ?? ''));
884 if ($keyPem === '') $keyPem = $certPem; // 証明書と鍵が同じファイル
885 $certs = X509::allFromPem($certPem);
886 if (empty($certs)) throw new TlsException('TLS:サーバーの証明書がありません', self::ALERT_INTERNAL_ERROR);
887 $key = str_contains($keyPem, 'EC PRIVATE KEY') ? Ec::fromPem($keyPem) : null;
888 if ($key === null) {
889 try {
890 $key = Rsa::fromPem($keyPem);
891 } catch (\InvalidArgumentException $e) {
892 $key = Ec::fromPem($keyPem);
893 }
894 }
895 if (!$key->isPrivate()) throw new TlsException('TLS:サーバーの秘密鍵がありません', self::ALERT_INTERNAL_ERROR);
896 if ($key->publicDer() !== $certs[0]->publicKey()->publicDer()) throw new TlsException('TLS:証明書と秘密鍵が合いません', self::ALERT_INTERNAL_ERROR);
897 return [$certs, $key];
898 }
899
900 protected function parseClientHello(string $b): array {
901 $pos = 2;
902 $random = substr($b, $pos, 32);
903 $pos += 32;
904 $sid = $this->vec($b, $pos, 1, $pos);
905 $suites = array_values(unpack('n*', $this->vec($b, $pos, 2, $pos)) ?: []);
906 $comp = $this->vec($b, $pos, 1, $pos);
907 $ext = $this->parseExtensions($pos < strlen($b) ? $this->vec($b, $pos, 2, $pos) : '');
908 if ($pos !== strlen($b) || strlen($random) !== 32 || strlen($sid) > 32) throw new TlsException('TLS:不正なClientHello', self::ALERT_DECODE_ERROR);
909 if (!str_contains($comp, "\x00")) throw new TlsException('TLS:不正なClientHello', self::ALERT_ILLEGAL_PARAMETER);
910
911 $versions = isset($ext[self::EXT_SUPPORTED_VERSIONS])
912 ? array_values(unpack('n*', $this->vec($ext[self::EXT_SUPPORTED_VERSIONS], 0, 1)) ?: []) : [];
913
914 $shares = null;
915 if (isset($ext[self::EXT_KEY_SHARE])) {
916 $shares = [];
917 $ks = $this->vec($ext[self::EXT_KEY_SHARE], 0, 2);
918 for ($p = 0; $p < strlen($ks); ) {
919 $g = $this->u16($ks, $p);
920 $p += 2;
921 if (isset($shares[$g])) throw new TlsException('TLS:重複したkey_share', self::ALERT_ILLEGAL_PARAMETER);
922 $shares[$g] = $this->vec($ks, $p, 2, $p);
923 }
924 }
925
926 $alpn = [];
927 if (isset($ext[self::EXT_ALPN])) {
928 $l = $this->vec($ext[self::EXT_ALPN], 0, 2);
929 for ($p = 0; $p < strlen($l); ) $alpn[] = $this->vec($l, $p, 1, $p);
930 }
931 $sni = '';
932 if (isset($ext[self::EXT_SERVER_NAME])) {
933 $l = $this->vec($ext[self::EXT_SERVER_NAME], 0, 2);
934 if (($l[0] ?? '') === "\x00") $sni = $this->vec($l, 1, 2);
935 }
936
937 $list = fn(int $t): ?array => isset($ext[$t]) ? array_values(unpack('n*', $this->vec($ext[$t], 0, 2)) ?: []) : null;
938 return [
939 'legacy' => $this->u16($b, 0),
940 'versions' => $versions,
941 'random' => $random,
942 'sid' => $sid,
943 'suites' => $suites,
944 'groups' => $list(self::EXT_SUPPORTED_GROUPS),
945 'sigs' => $list(self::EXT_SIGNATURE_ALGORITHMS),
946 'shares' => $shares,
947 'alpn' => $alpn,
948 'sni' => $sni,
949 'ext' => $ext,
950 ];
951 }
952
953 /** サーバーの鍵とクライアントの署名の方式から選ぶ */
954 private function chooseScheme(Rsa|Ec $key, array $sigs): int {
955 foreach ($sigs as $s) {
956 $d = self::SIG_SCHEMES[$s] ?? null;
957 if ($d === null) continue;
958 if ($key instanceof Rsa && $d[0] === 'pss') return $s;
959 if ($key instanceof Ec && $d[0] === 'ec' && $d[2] === $key->curve) return $s;
960 }
961 throw new TlsException('TLS:共通の署名の方式がありません', self::ALERT_HANDSHAKE_FAILURE);
962 }
963
964 /** TLS 1.2の署名の方式(クライアントの順、ECDSAはどの曲線でも良い) */
965 private function chooseScheme12(Rsa|Ec $key, array $sigs): int {
966 foreach ($sigs as $s) {
967 $d = self::SIG_SCHEMES[$s] ?? null;
968 if ($d === null) continue;
969 if ($key instanceof Rsa && ($d[0] === 'pss' || $d[0] === 'pkcs1')) return $s;
970 if ($key instanceof Ec && $d[0] === 'ec') return $s;
971 }
972 throw new TlsException('TLS:共通の署名の方式がありません', self::ALERT_HANDSHAKE_FAILURE);
973 }
974
975 ///////////////
976 // TLS 1.2の鍵・署名(RFC 5246、RFC 7627)
977
978 /** PRF(P_hash) */
979 private function prf(string $secret, string $label, string $seed, int $len): string {
980 $seed = $label.$seed;
981 $a = $seed;
982 $out = '';
983 while (strlen($out) < $len) {
984 $a = hash_hmac($this->hash, $a, $secret, true);
985 $out .= hash_hmac($this->hash, $a.$seed, $secret, true);
986 }
987 return substr($out, 0, $len);
988 }
989
990 /** マスターシークレット(相手が対応していればExtended Master Secret) */
991 private function masterSecret(string $pms, string $cr, string $sr, bool $ems): string {
992 return $ems
993 ? $this->prf($pms, 'extended master secret', hash($this->hash, $this->transcript, true), 48)
994 : $this->prf($pms, 'master secret', $cr.$sr, 48);
995 }
996
997 /** 鍵を作る(書く方は自分のChangeCipherSpecの後、読む方は相手のChangeCipherSpecで使い始める) */
998 private function keys12(string $master, string $cr, string $sr): void {
999 [$type, $keyLen] = self::SUITES12[$this->suite];
1000 $ivLen = $type === 'aes' ? 4 : 12;
1001 $kb = $this->prf($master, 'key expansion', $sr.$cr, 2 * $keyLen + 2 * $ivLen);
1002 $c = [substr($kb, 0, $keyLen), substr($kb, 2 * $keyLen, $ivLen)];
1003 $s = [substr($kb, $keyLen, $keyLen), substr($kb, 2 * $keyLen + $ivLen, $ivLen)];
1004 $make = fn(array $k): array => [$type === 'aes' ? new Aes($k[0]) : new ChaCha20($k[0]), $k[1]];
1005 $this->pendingRead = $make($this->server ? $c : $s);
1006 $this->pendingWrite = $make($this->server ? $s : $c);
1007 }
1008
1009 private function activateWrite12(): void {
1010 [$this->writeAead, $this->writeIv] = $this->pendingWrite;
1011 $this->pendingWrite = null;
1012 $this->writeSeq = 0;
1013 }
1014
1015 /** TLS 1.2のFinished */
1016 protected function finished12(string $master, string $label): string {
1017 return $this->prf($master, $label, hash($this->hash, $this->transcript, true), 12);
1018 }
1019
1020 /** ServerKeyExchangeの署名 */
1021 protected function signKeyExchange(Rsa|Ec $key, int $scheme, string $data): string {
1022 [$type, $hash] = self::SIG_SCHEMES[$scheme];
1023 return $type === 'pss' ? $key->signPss($data, $hash) : $key->sign($data, $hash);
1024 }
1025
1026 /** ServerKeyExchangeの署名を確認する(送った方式だけ、鍵の種類も合う事) */
1027 private function verify12(Rsa|Ec $key, int $scheme, string $data, string $sig): bool {
1028 $d = self::SIG_SCHEMES[$scheme] ?? null;
1029 return match (true) {
1030 $d === null => false,
1031 $d[0] === 'pss' && $key instanceof Rsa => $key->verifyPss($data, $sig, $d[1]),
1032 $d[0] === 'pkcs1' && $key instanceof Rsa => $key->verify($data, $sig, $d[1]),
1033 $d[0] === 'ec' && $key instanceof Ec => $key->verify($data, $sig, $d[1]),
1034 default => false,
1035 };
1036 }
1037
1038 /** TLS 1.2のCertificate(拡張なし) */
1039 private function parseCertificate12(string $body): array {
1040 $list = $this->vec($body, 0, 3, $end);
1041 if ($end !== strlen($body)) throw new TlsException('TLS:不正なCertificate', self::ALERT_DECODE_ERROR);
1042 $certs = [];
1043 for ($i = 0; $i < strlen($list); ) {
1044 try {
1045 $certs[] = new X509($this->vec($list, $i, 3, $i));
1046 } catch (\UnexpectedValueException $e) {
1047 throw new TlsException('TLS:壊れた証明書:'.$e->getMessage(), self::ALERT_BAD_CERTIFICATE, false, $e);
1048 }
1049 }
1050 return $certs;
1051 }
1052
1053 ///////////////
1054 // 鍵交換・署名
1055
1056 private function newShare(int $group): array {
1057 $name = array_search($group, self::GROUPS, true);
1058 $fixed = $this->opt['_keys'][$name] ?? null;
1059 if ($group === self::GROUPS['x25519']) {
1060 $priv = $fixed ?? random_bytes(32);
1061 return ['priv' => $priv, 'pub' => X25519::publicKey($priv)];
1062 }
1063 $k = $fixed !== null ? Ec::fromPem($fixed) : Ec::generate($name);
1064 return ['priv' => $k, 'pub' => $k->publicPoint()];
1065 }
1066
1067 private function sharedSecret(array $mine, int $group, string $peer): string {
1068 try {
1069 if ($group === self::GROUPS['x25519']) return X25519::sharedSecret($mine['priv'], $peer);
1070 return $mine['priv']->ecdh($peer);
1071 } catch (\InvalidArgumentException $e) {
1072 throw new TlsException('TLS:不正な鍵:'.$e->getMessage(), self::ALERT_ILLEGAL_PARAMETER, false, $e);
1073 }
1074 }
1075
1076 private function signedContent(string $context): string {
1077 return str_repeat(' ', 64).$context."\x00".hash($this->hash, $this->transcript, true);
1078 }
1079
1080 protected function signTranscript(Rsa|Ec $key, int $scheme, string $context): string {
1081 [$type, $hash] = self::SIG_SCHEMES[$scheme];
1082 $content = $this->signedContent($context);
1083 return $type === 'pss' ? $key->signPss($content, $hash) : $key->sign($content, $hash);
1084 }
1085
1086 private function checkCertificateVerify(string $body, X509 $cert, string $context): void {
1087 $scheme = $this->u16($body, 0);
1088 $p = 2;
1089 $sig = $this->vec($body, $p, 2, $p);
1090 if ($p !== strlen($body)) throw new TlsException('TLS:不正なCertificateVerify', self::ALERT_DECODE_ERROR);
1091 $d = self::SIG_SCHEMES[$scheme] ?? null;
1092 // TLS 1.3のCertificateVerifyにPKCS#1 v1.5は使えない
1093 if ($d === null || $d[0] === 'pkcs1') throw new TlsException('TLS:不正な署名の方式', self::ALERT_ILLEGAL_PARAMETER);
1094
1095 try {
1096 $key = $cert->publicKey();
1097 } catch (\Throwable $e) {
1098 throw new TlsException('TLS:未対応の証明書の鍵', self::ALERT_BAD_CERTIFICATE, false, $e);
1099 }
1100 $content = $this->signedContent($context);
1101 $ok = match (true) {
1102 $d[0] === 'pss' && $key instanceof Rsa => $key->verifyPss($content, $sig, $d[1]),
1103 $d[0] === 'ec' && $key instanceof Ec && $key->curve === $d[2] => $key->verify($content, $sig, $d[1]),
1104 default => false,
1105 };
1106 if (!$ok) throw new TlsException('TLS:CertificateVerifyの署名が不正です', self::ALERT_DECRYPT_ERROR);
1107 }
1108
1109 private function parseCertificate(string $body): array {
1110 $p = 0;
1111 $this->vec($body, $p, 1, $p); // certificate_request_context
1112 $list = $this->vec($body, $p, 3, $p);
1113 if ($p !== strlen($body)) throw new TlsException('TLS:不正なCertificate', self::ALERT_DECODE_ERROR);
1114 $certs = [];
1115 for ($i = 0; $i < strlen($list); ) {
1116 $der = $this->vec($list, $i, 3, $i);
1117 $this->vec($list, $i, 2, $i); // 拡張
1118 try {
1119 $certs[] = new X509($der);
1120 } catch (\UnexpectedValueException $e) {
1121 throw new TlsException('TLS:壊れた証明書:'.$e->getMessage(), self::ALERT_BAD_CERTIFICATE, false, $e);
1122 }
1123 }
1124 return $certs;
1125 }
1126
1127 ///////////////
1128 // 鍵のスケジュール(RFC 8446 7.1)
1129
1130 private function handshakeSecrets(string $shared): array {
1131 $len = strlen(hash($this->hash, '', true));
1132 $zero = str_repeat("\0", $len);
1133 $early = hash_hmac($this->hash, $zero, $zero, true);
1134 $hs = hash_hmac($this->hash, $shared, $this->deriveSecret($early, 'derived', ''), true);
1135 $th = hash($this->hash, $this->transcript, true);
1136 return [
1137 $this->expandLabel($hs, 'c hs traffic', $th, $len),
1138 $this->expandLabel($hs, 's hs traffic', $th, $len),
1139 hash_hmac($this->hash, $zero, $this->deriveSecret($hs, 'derived', ''), true),
1140 ];
1141 }
1142
1143 private function appSecrets(string $master): array {
1144 $len = strlen(hash($this->hash, '', true));
1145 $th = hash($this->hash, $this->transcript, true);
1146 return [$this->expandLabel($master, 'c ap traffic', $th, $len), $this->expandLabel($master, 's ap traffic', $th, $len)];
1147 }
1148
1149 private function deriveSecret(string $secret, string $label, string $messages): string {
1150 return $this->expandLabel($secret, $label, hash($this->hash, $messages, true), strlen(hash($this->hash, '', true)));
1151 }
1152
1153 /** HKDF-Expand-Label */
1154 public function expandLabel(string $secret, string $label, string $context, int $len): string {
1155 $info = pack('n', $len).$this->vec2('tls13 '.$label, 1).$this->vec2($context, 1);
1156 $out = '';
1157 $t = '';
1158 for ($i = 1; strlen($out) < $len; $i++) {
1159 $t = hash_hmac($this->hash, $t.$info.chr($i), $secret, true);
1160 $out .= $t;
1161 }
1162 return substr($out, 0, $len);
1163 }
1164
1165 protected function finished(string $base): string {
1166 $key = $this->expandLabel($base, 'finished', '', strlen(hash($this->hash, '', true)));
1167 return hash_hmac($this->hash, hash($this->hash, $this->transcript, true), $key, true);
1168 }
1169
1170 /** HelloRetryRequestの後の最初のClientHelloの代わり */
1171 private function messageHash(string $hello): string {
1172 $h = hash($this->hash, $hello, true);
1173 return chr(self::MESSAGE_HASH)."\x00\x00".chr(strlen($h)).$h;
1174 }
1175
1176 private function aead(string $secret): array {
1177 [$type, $keyLen] = self::SUITES[$this->suite];
1178 $key = $this->expandLabel($secret, 'key', '', $keyLen);
1179 $iv = $this->expandLabel($secret, 'iv', '', 12);
1180 return [$type === 'aes' ? new Aes($key) : new ChaCha20($key), $iv];
1181 }
1182
1183 private function setReadKey(string $secret): void {
1184 if ($this->hs !== '') throw new TlsException('TLS:鍵の変更の前に余計なデータ', self::ALERT_UNEXPECTED_MESSAGE);
1185 [$this->readAead, $this->readIv] = $this->aead($secret);
1186 $this->readSecret = $secret;
1187 $this->readSeq = 0;
1188 }
1189
1190 private function setWriteKey(string $secret): void {
1191 [$this->writeAead, $this->writeIv] = $this->aead($secret);
1192 $this->writeSecret = $secret;
1193 $this->writeSeq = 0;
1194 }
1195
1196 private function nonce(string $iv, int $seq): string {
1197 return $iv ^ (str_repeat("\0", 4).pack('J', $seq));
1198 }
1199
1200 ///////////////
1201 // レコード層
1202
1203 /**
1204 * レコードを書く(16KiB毎に分ける)
1205 *
1206 * @param int $legacy レコードの古いバージョン(最初のClientHelloは0x0301)
1207 */
1208 protected function writeRecord(int $type, string $data, int $legacy = 0x0303): void {
1209 $out = '';
1210 $pos = 0;
1211 do {
1212 $chunk = substr($data, $pos, self::MAX_PLAIN);
1213 $pos += self::MAX_PLAIN;
1214 if ($this->writeAead === null) {
1215 $out .= chr($type).pack('n', $legacy).pack('n', strlen($chunk)).$chunk;
1216 continue;
1217 }
1218 if ($this->version === 0x0303) {
1219 // TLS 1.2:AADは順序番号・種類・バージョン・長さ。AES-GCMは明示的なノンス(順序番号)を付ける(RFC 5288)、
1220 // ChaCha20はTLS 1.3と同じノンス(RFC 7905)
1221 $seq = pack('J', $this->writeSeq);
1222 $aad = $seq.chr($type)."\x03\x03".pack('n', strlen($chunk));
1223 $sealed = $this->writeAead instanceof Aes
1224 ? $seq.$this->writeAead->seal($this->writeIv.$seq, $chunk, $aad)
1225 : $this->writeAead->seal($this->nonce($this->writeIv, $this->writeSeq), $chunk, $aad);
1226 $out .= chr($type)."\x03\x03".pack('n', strlen($sealed)).$sealed;
1227 $this->writeSeq++;
1228 continue;
1229 }
1230 $inner = $chunk.chr($type);
1231 $header = chr(self::APPDATA)."\x03\x03".pack('n', strlen($inner) + 16);
1232 $out .= $header.$this->writeAead->seal($this->nonce($this->writeIv, $this->writeSeq), $inner, $header);
1233 $this->writeSeq++;
1234 } while ($pos < strlen($data));
1235 $this->send($out);
1236 }
1237
1238 private function writeCcs(): void {
1239 $this->send("\x14\x03\x03\x00\x01\x01");
1240 }
1241
1242 private function send(string $raw): void {
1243 for ($w = 0, $len = strlen($raw); $w < $len; ) {
1244 $n = @fwrite($this->sock, substr($raw, $w, 65536));
1245 if ($n === false || $n === 0) {
1246 $this->rawEof = true;
1247 throw new TlsException('TLS:送信に失敗(接続が切れました)', self::ALERT_INTERNAL_ERROR, true);
1248 }
1249 $w += $n;
1250 }
1251 }
1252
1253 private function sendAlert(int $alert, int $level = 2): void {
1254 if ($this->closed || !is_resource($this->sock)) return;
1255 try {
1256 $this->writeRecord(self::ALERT, chr($level).chr($alert));
1257 } catch (\Throwable $e) {
1258 // 送れなくても構わない
1259 }
1260 }
1261
1262 /** ソケットから$nバイトを溜める(タイムアウト・終わりならfalse) */
1263 private function fill(int $n): bool {
1264 while (strlen($this->in) < $n) {
1265 $chunk = @fread($this->sock, max(8192, $n - strlen($this->in)));
1266 if ($chunk === false || $chunk === '') {
1267 if (!is_resource($this->sock) || feof($this->sock)) {
1268 $this->rawEof = true;
1269 return false;
1270 }
1271 if (stream_get_meta_data($this->sock)['timed_out'] ?? false) {
1272 $this->timedOut = true;
1273 return false;
1274 }
1275 continue;
1276 }
1277 $this->in .= $chunk;
1278 }
1279 return true;
1280 }
1281
1282 /**
1283 * レコードを1つ読む
1284 *
1285 * @return array|null [種類, 中身]、タイムアウト・終わりならnull
1286 * @throws TlsException
1287 */
1288 private function readRecord(): ?array {
1289 $this->timedOut = false;
1290 if (!$this->fill(5)) return null;
1291 $type = ord($this->in[0]);
1292 $len = $this->u16($this->in, 3);
1293 if ($len > self::MAX_PLAIN + ($this->version === 0x0303 ? 2048 : 256)) throw new TlsException('TLS:レコードが大き過ぎます', self::ALERT_RECORD_OVERFLOW);
1294 if (!in_array($type, [self::CCS, self::ALERT, self::HANDSHAKE, self::APPDATA], true)) {
1295 throw new TlsException('TLS:不正なレコード', self::ALERT_UNEXPECTED_MESSAGE);
1296 }
1297 if (!$this->fill(5 + $len)) return null;
1298 $header = substr($this->in, 0, 5);
1299 $body = substr($this->in, 5, $len);
1300 $this->in = (string)substr($this->in, 5 + $len);
1301
1302 if ($type === self::CCS) {
1303 if ($this->established || $body !== "\x01") throw new TlsException('TLS:不正なChangeCipherSpec', self::ALERT_UNEXPECTED_MESSAGE);
1304 if ($this->version === 0x0303) {
1305 // TLS 1.2:ここから相手の鍵を使う(ハンドシェイクのメッセージの途中では駄目)
1306 if ($this->pendingRead === null || $this->hs !== '') throw new TlsException('TLS:想定外のChangeCipherSpec', self::ALERT_UNEXPECTED_MESSAGE);
1307 [$this->readAead, $this->readIv] = $this->pendingRead;
1308 $this->pendingRead = null;
1309 $this->readSeq = 0;
1310 }
1311 // TLS 1.3:互換性のためのChangeCipherSpecは無視する
1312 return $this->readRecord();
1313 }
1314 if ($this->readAead === null) {
1315 if ($type === self::APPDATA) throw new TlsException('TLS:暗号化前のアプリケーションデータ', self::ALERT_UNEXPECTED_MESSAGE);
1316 if ($len > self::MAX_PLAIN) throw new TlsException('TLS:レコードが大き過ぎます', self::ALERT_RECORD_OVERFLOW);
1317 return [$type, $body];
1318 }
1319 if ($this->version === 0x0303) return $this->decrypt12($type, $body);
1320 if ($type !== self::APPDATA) {
1321 if ($type === self::ALERT) return [$type, $body]; // 暗号化前のアラート(相手がこちらの鍵を持つ前に失敗した)
1322 throw new TlsException('TLS:暗号化されていないレコード', self::ALERT_UNEXPECTED_MESSAGE);
1323 }
1324
1325 $plain = $this->readAead->open($this->nonce($this->readIv, $this->readSeq), $body, $header);
1326 if ($plain === null) throw new TlsException('TLS:レコードの復号に失敗(改ざん?)', self::ALERT_BAD_RECORD_MAC);
1327 $this->readSeq++;
1328 $plain = rtrim($plain, "\0");
1329 if ($plain === '') throw new TlsException('TLS:レコードの種類がありません', self::ALERT_UNEXPECTED_MESSAGE);
1330 $inner = ord(substr($plain, -1));
1331 $plain = substr($plain, 0, -1);
1332 if (strlen($plain) > self::MAX_PLAIN) throw new TlsException('TLS:レコードが大き過ぎます', self::ALERT_RECORD_OVERFLOW);
1333 return [$inner, $plain];
1334 }
1335
1336 /** TLS 1.2のレコードを復号する */
1337 private function decrypt12(int $type, string $body): array {
1338 $seq = pack('J', $this->readSeq);
1339 if ($this->readAead instanceof Aes) {
1340 if (strlen($body) < 24) throw new TlsException('TLS:レコードが短過ぎます', self::ALERT_BAD_RECORD_MAC);
1341 $ct = substr($body, 8);
1342 $plain = $this->readAead->open($this->readIv.substr($body, 0, 8), $ct, $seq.chr($type)."\x03\x03".pack('n', strlen($ct) - 16));
1343 } else {
1344 if (strlen($body) < 16) throw new TlsException('TLS:レコードが短過ぎます', self::ALERT_BAD_RECORD_MAC);
1345 $plain = $this->readAead->open($this->nonce($this->readIv, $this->readSeq), $body, $seq.chr($type)."\x03\x03".pack('n', strlen($body) - 16));
1346 }
1347 if ($plain === null) throw new TlsException('TLS:レコードの復号に失敗(改ざん?)', self::ALERT_BAD_RECORD_MAC);
1348 $this->readSeq++;
1349 if (strlen($plain) > self::MAX_PLAIN) throw new TlsException('TLS:レコードが大き過ぎます', self::ALERT_RECORD_OVERFLOW);
1350 return [$type, $plain];
1351 }
1352
1353 /**
1354 * ハンドシェイクのメッセージを1つ読む
1355 *
1356 * @return array [種類, 中身]
1357 * @throws TlsException
1358 */
1359 private function readHandshake(): array {
1360 while (true) {
1361 if (strlen($this->hs) >= 4) {
1362 $len = unpack('N', "\0".substr($this->hs, 1, 3))[1];
1363 if ($len > 0x10000 * 4) throw new TlsException('TLS:ハンドシェイクのメッセージが大き過ぎます', self::ALERT_DECODE_ERROR);
1364 if (strlen($this->hs) >= 4 + $len) {
1365 $type = ord($this->hs[0]);
1366 $body = substr($this->hs, 4, $len);
1367 $this->hs = (string)substr($this->hs, 4 + $len);
1368 return [$type, $body];
1369 }
1370 }
1371 $rec = $this->readRecord();
1372 if ($rec === null) {
1373 throw new TlsException($this->timedOut ? 'TLS:ハンドシェイクがタイムアウトしました' : 'TLS:ハンドシェイク中に接続が切れました', self::ALERT_INTERNAL_ERROR, true);
1374 }
1375 [$type, $body] = $rec;
1376 if ($type === self::ALERT) $this->alert($body);
1377 if ($type !== self::HANDSHAKE || $body === '') throw new TlsException('TLS:ハンドシェイクを期待しました', self::ALERT_UNEXPECTED_MESSAGE);
1378 $this->hs .= $body;
1379 }
1380 }
1381
1382 /** 接続後のレコードを1つ処理する(データが来たらtrue、タイムアウト・終わりならfalse) */
1383 private function readRecordInto(): bool {
1384 try {
1385 $rec = $this->readRecord();
1386 if ($rec === null) return false;
1387 [$type, $body] = $rec;
1388
1389 if ($type === self::APPDATA) {
1390 $this->app .= $body;
1391 return true;
1392 }
1393 if ($type === self::ALERT) {
1394 $this->alert($body);
1395 return true;
1396 }
1397 // 接続後のハンドシェイク
1398 $this->hs .= $body;
1399 while (strlen($this->hs) >= 4) {
1400 $len = unpack('N', "\0".substr($this->hs, 1, 3))[1];
1401 if (strlen($this->hs) < 4 + $len) break;
1402 $mt = ord($this->hs[0]);
1403 $mb = substr($this->hs, 4, $len);
1404 $this->hs = (string)substr($this->hs, 4 + $len);
1405 if ($this->version === 0x0303) {
1406 // TLS 1.2:再ネゴシエーションは断る(警告のno_renegotiation)
1407 if (($mt === self::HELLO_REQUEST && !$this->server) || ($mt === self::CLIENT_HELLO && $this->server)) {
1408 $this->sendAlert(self::ALERT_NO_RENEGOTIATION, 1);
1409 continue;
1410 }
1411 throw new TlsException('TLS:想定外のハンドシェイクのメッセージ', self::ALERT_UNEXPECTED_MESSAGE);
1412 }
1413 if ($mt === self::NEW_SESSION_TICKET && !$this->server) continue; // 再開は未対応
1414 if ($mt === self::KEY_UPDATE && strlen($mb) === 1 && ord($mb) <= 1) {
1415 if ($this->hs !== '') throw new TlsException('TLS:KeyUpdateの後に余計なデータ', self::ALERT_UNEXPECTED_MESSAGE);
1416 $len2 = strlen(hash($this->hash, '', true));
1417 $this->setReadKey($this->expandLabel($this->readSecret, 'traffic upd', '', $len2));
1418 if (ord($mb) === 1) {
1419 $this->writeRecord(self::HANDSHAKE, $this->msg(self::KEY_UPDATE, "\x00"));
1420 $this->setWriteKey($this->expandLabel($this->writeSecret, 'traffic upd', '', $len2));
1421 }
1422 continue;
1423 }
1424 throw new TlsException('TLS:想定外のハンドシェイクのメッセージ', self::ALERT_UNEXPECTED_MESSAGE);
1425 }
1426 return true;
1427 } catch (TlsException $e) {
1428 if (!$e->received) $this->sendAlert($e->alert);
1429 $this->error = $e;
1430 return false;
1431 }
1432 }
1433
1434 /**
1435 * 鍵を更新する(KeyUpdate)
1436 *
1437 * @param bool $requestPeer 相手にも更新を求めるかどうか
1438 * @return void
1439 */
1440 public function keyUpdate(bool $requestPeer = false): void {
1441 if ($this->version !== 0x0304) throw new \LogicException('TLS:KeyUpdateはTLS 1.3だけ');
1442 $this->writeRecord(self::HANDSHAKE, $this->msg(self::KEY_UPDATE, $requestPeer ? "\x01" : "\x00"));
1443 $this->setWriteKey($this->expandLabel($this->writeSecret, 'traffic upd', '', strlen(hash($this->hash, '', true))));
1444 }
1445
1446 /** 受け取ったアラート */
1447 private function alert(string $body): void {
1448 if (strlen($body) !== 2) throw new TlsException('TLS:不正なアラート', self::ALERT_DECODE_ERROR);
1449 $code = ord($body[1]);
1450 if ($code === self::ALERT_CLOSE_NOTIFY) {
1451 $this->peerClosed = true;
1452 return;
1453 }
1454 if ($code === self::ALERT_USER_CANCELED) return;
1455 // TLS 1.2の警告(no_renegotiation等)は無視する
1456 if ($this->version === 0x0303 && ord($body[0]) === 1) return;
1457 if ($code === self::ALERT_PROTOCOL_VERSION && !$this->established) {
1458 throw new TlsException('TLS:相手が対応しているバージョンがありません(protocol_version)', $code, true);
1459 }
1460 throw new TlsException("TLS:相手からのアラート:{$code}(".self::alertName($code).')', $code, true);
1461 }
1462
1463 /** アラートの名前 */
1464 public static function alertName(int $code): string {
1465 static $names = null;
1466 if ($names === null) {
1467 $names = [];
1468 foreach ((new \ReflectionClass(self::class))->getConstants() as $k => $v) {
1469 if (str_starts_with($k, 'ALERT_')) $names[$v] = strtolower(substr($k, 6));
1470 }
1471 }
1472 return $names[$code] ?? '不明';
1473 }
1474
1475 ///////////////
1476 // 符号化
1477
1478 private function msg(int $type, string $body): string {
1479 return chr($type).substr(pack('N', strlen($body)), 1).$body;
1480 }
1481
1482 private function ext(int $type, string $data): string {
1483 return pack('n', $type).$this->vec2($data, 2);
1484 }
1485
1486 /** 長さ($bytesバイト)付きのデータ */
1487 private function vec2(string $data, int $bytes): string {
1488 return substr(pack('N', strlen($data)), 4 - $bytes).$data;
1489 }
1490
1491 /** 長さ付きのデータを読む */
1492 private function vec(string $b, int $pos, int $bytes, ?int &$end = null): string {
1493 if ($pos + $bytes > strlen($b)) throw new TlsException('TLS:メッセージが短過ぎます', self::ALERT_DECODE_ERROR);
1494 $len = unpack('N', str_repeat("\0", 4 - $bytes).substr($b, $pos, $bytes))[1];
1495 if ($pos + $bytes + $len > strlen($b)) throw new TlsException('TLS:メッセージが短過ぎます', self::ALERT_DECODE_ERROR);
1496 $end = $pos + $bytes + $len;
1497 return (string)substr($b, $pos + $bytes, $len);
1498 }
1499
1500 private function u16(string $b, int $pos): int {
1501 if ($pos + 2 > strlen($b)) throw new TlsException('TLS:メッセージが短過ぎます', self::ALERT_DECODE_ERROR);
1502 return unpack('n', $b, $pos)[1];
1503 }
1504
1505 /** 拡張の一覧 => [種類 => 中身] */
1506 private function parseExtensions(string $data): array {
1507 $out = [];
1508 for ($p = 0; $p < strlen($data); ) {
1509 $t = $this->u16($data, $p);
1510 $p += 2;
1511 if (isset($out[$t])) throw new TlsException('TLS:重複した拡張', self::ALERT_ILLEGAL_PARAMETER);
1512 $out[$t] = $this->vec($data, $p, 2, $p);
1513 }
1514 return $out;
1515 }
1516}