master
ブランチ master
LittleBeast / src / Std / Test / LibTls.php
24.4 KB、556行 生 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\Test;
39
40require_once __DIR__.'/../../../autoload.php';
41
42use Std\Lib\Tester;
43use Std\Lib\Tls;
44use Std\Lib\Tls\Connection;
45use Std\Lib\Tls\Stream;
46use Std\Lib\Tls\TlsException;
47use Std\Test\Helper\MockServer;
48use Std\Test\Helper\TestCert;
49
50$test = new Tester([
51 'colorOutput' => true,
52 'verboseOutput' => true
53]);
54
55// 記録したデータを返し、書いたデータを記録するソケット(OpenSSLとの記録を再現する為)
56final class LibTlsReplay {
57 public $context;
58 public static string $in = '';
59 public static string $out = '';
60 private int $pos = 0;
61
62 public function stream_open(string $path, string $mode, int $options, ?string &$opened): bool {
63 $this->pos = 0;
64 return true;
65 }
66 public function stream_read(int $n): string {
67 $d = (string)substr(self::$in, $this->pos, $n);
68 $this->pos += strlen($d);
69 return $d;
70 }
71 public function stream_write(string $d): int {
72 self::$out .= $d;
73 return strlen($d);
74 }
75 public function stream_eof(): bool {
76 return $this->pos >= strlen(self::$in);
77 }
78 public function stream_close(): void {}
79 public function stream_set_option(int $o, int $a, ?int $b): bool {
80 return false;
81 }
82}
83
84$catch = function(callable $fn): ?\Throwable {
85 try {
86 $fn();
87 } catch (\Throwable $e) {
88 return $e;
89 }
90 return null;
91};
92
93$test->describe('TLS 1.3・1.2(PHP純正のクライアントとサーバー)', function($test) use ($catch): void {
94 $ec = TestCert::get('ec');
95 $rsa = TestCert::get('rsa');
96 $self = TestCert::get('self');
97 $srv = MockServer::start('tls-server.php', [$ec['cert'], $ec['key'], 'h2,http/1.1']);
98 $srvRsa = MockServer::start('tls-server.php', [$rsa['cert'], $rsa['key']]);
99 $srvP384 = MockServer::start('tls-server.php', [$ec['cert'], $ec['key'], '', 'P-384']);
100 $srvSelf = MockServer::start('tls-server.php', [$self['cert'], $self['key']]);
101 if (!$srv || !$srvRsa || !$srvP384 || !$srvSelf) {
102 $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効?)');
103 return;
104 }
105
106 $open = fn(MockServer $s, array $opt = []) => Tls::connect('127.0.0.1', $s->port, $opt + ['peer_name' => 'localhost', 'cafile' => $ec['ca']], 10);
107 $info = function($s): array {
108 fwrite($s, "INFO\n");
109 return json_decode((string)fgets($s), true);
110 };
111
112 $test->it('接続して送受信出来るはず', function($test) use ($srv, $open): void {
113 $s = $open($srv);
114 fwrite($s, "こんにちは\n");
115 $test->assertSame("こんにちは\n", fgets($s));
116 $i = Tls::info($s);
117 $test->assertSame('TLSv1.3', $i['version']);
118 $test->assertSame('TLS_CHACHA20_POLY1305_SHA256', $i['cipher']);
119 $test->assertSame('x25519', $i['group']);
120 $test->assertSame('CN=localhost', $i['peer_certificates'][0]->subject);
121 $test->assertFalse(feof($s));
122 fclose($s);
123 $test->assertNull(Tls::info(fopen('php://memory', 'r')));
124 });
125
126 $test->it('全部の暗号スイートとグループで繋がるはず', function($test) use ($srv, $open, $info): void {
127 foreach (Connection::SUITES as $id => [, , , $name]) {
128 $s = $open($srv, ['ciphers' => [$id]]);
129 $test->assertSame($name, $info($s)['cipher'], $name);
130 fclose($s);
131 }
132 foreach (array_keys(Connection::GROUPS) as $g) {
133 $s = $open($srv, ['groups' => [$g]]);
134 $test->assertSame($g, $info($s)['group'], $g);
135 fclose($s);
136 }
137 });
138
139 $test->it('サーバーが別のグループを求めたら(HelloRetryRequest)送り直すはず', function($test) use ($srvP384, $open, $info, $catch): void {
140 $s = $open($srvP384);
141 $test->assertSame('P-384', Tls::info($s)['group']);
142 $test->assertSame('P-384', $info($s)['group']);
143 fwrite($s, "after hrr\n");
144 $test->assertSame("after hrr\n", fgets($s));
145 fclose($s);
146
147 $e = $catch(fn() => $open($srvP384, ['groups' => ['x25519', 'P-256']]));
148 $test->assertTrue($e instanceof TlsException);
149 $test->assertSame(Connection::ALERT_HANDSHAKE_FAILURE, $e->alert);
150 });
151
152 $test->it('RSAの証明書(RSA-PSSの署名)でも繋がるはず', function($test) use ($srvRsa, $open): void {
153 $s = $open($srvRsa);
154 fwrite($s, "rsa\n");
155 $test->assertSame("rsa\n", fgets($s));
156 $test->assertSame('localhost', Tls::info($s)['peer_certificates'][0]->subjectAttr('CN')[0]);
157 fclose($s);
158 });
159
160 $test->it('ALPNとSNIを送るはず', function($test) use ($srv, $open, $info, $catch): void {
161 $s = $open($srv, ['alpn' => ['http/1.1']]);
162 $test->assertSame('http/1.1', Tls::info($s)['alpn']);
163 $i = $info($s);
164 $test->assertSame('http/1.1', $i['alpn']);
165 $test->assertSame('localhost', $i['server_name']);
166 fclose($s);
167
168 // IPアドレスにはSNIを送らない
169 $s = $open($srv, ['peer_name' => '127.0.0.1']);
170 $test->assertSame('', $info($s)['server_name']);
171 fclose($s);
172
173 $e = $catch(fn() => $open($srv, ['alpn' => ['spdy/3']]));
174 $test->assertTrue($e instanceof TlsException);
175 $test->assertSame(Connection::ALERT_NO_APPLICATION_PROTOCOL, $e->alert);
176 });
177
178 $test->it('大きいデータを送受信出来るはず(複数のレコード)', function($test) use ($srv, $open): void {
179 $s = $open($srv);
180 fwrite($s, "BIG 300000\n");
181 $got = '';
182 while (strlen($got) < 300000 && !feof($s)) $got .= fread($s, 65536);
183 $test->assertSame(str_repeat('x', 300000), $got);
184
185 $line = str_repeat('あいうえお', 20000)."\n";
186 fwrite($s, $line);
187 $test->assertSame($line, stream_get_line($s, 1000000, "\n")."\n");
188 fclose($s);
189 });
190
191 $test->it('鍵を更新(KeyUpdate)しても続けられるはず', function($test) use ($srv, $open): void {
192 $s = $open($srv);
193 fwrite($s, "KEYUPDATE\n");
194 $test->assertSame("updated\n", fgets($s));
195 fwrite($s, "after\n");
196 $test->assertSame("after\n", fgets($s));
197
198 Stream::connection($s)->keyUpdate(true);
199 fwrite($s, "mine\n");
200 $test->assertSame("mine\n", fgets($s));
201 fclose($s);
202 });
203
204 $test->it('相手が閉じたら終わりになるはず(close_notify、切断)', function($test) use ($srv, $open): void {
205 $s = $open($srv);
206 fwrite($s, "CLOSE\n");
207 $test->assertSame('', (string)fread($s, 10));
208 $test->assertTrue(feof($s));
209 $test->assertNull(Tls::error($s));
210 fclose($s);
211
212 $s = $open($srv);
213 fwrite($s, "DROP\n");
214 $test->assertSame('', (string)fread($s, 10));
215 $test->assertTrue(feof($s));
216 fclose($s);
217 });
218
219 $test->it('壊れたレコードを見つけるはず(bad_record_mac)', function($test) use ($srv, $open): void {
220 $s = $open($srv);
221 fwrite($s, "GARBAGE\n");
222 $test->assertSame('', (string)fread($s, 10));
223 $test->assertTrue(feof($s));
224 $test->assertSame(Connection::ALERT_BAD_RECORD_MAC, Tls::error($s)?->alert);
225 fclose($s);
226 });
227
228 $test->it('証明書を確認するはず', function($test) use ($srv, $srvSelf, $open, $catch, $self): void {
229 $e = $catch(fn() => $open($srv, ['peer_name' => 'example.com']));
230 $test->assertTrue($e instanceof TlsException);
231 $test->assertStringContains('ホスト名が一致しません', $e->getMessage());
232 $test->assertSame(Connection::ALERT_BAD_CERTIFICATE, $e->alert);
233
234 $e = $catch(fn() => $open($srvSelf));
235 $test->assertStringContains('自己署名', $e->getMessage());
236 $test->assertSame(Connection::ALERT_UNKNOWN_CA, $e->alert);
237
238 // 自己署名を許す、確認しない、ホスト名だけ確認しない
239 fclose($open($srvSelf, ['allow_self_signed' => true]));
240 fclose($open($srvSelf, ['verify_peer' => false]));
241 fclose($open($srv, ['peer_name' => 'example.com', 'verify_peer_name' => false]));
242 // 信頼する証明書として渡す
243 fclose($open($srvSelf, ['cafile' => $self['cert']]));
244 });
245
246 $test->it('タイムアウト・接続出来ない場合は例外のはず', function($test) use ($catch): void {
247 // 接続は受け付けるが、何も返さないサーバー
248 $quiet = stream_socket_server('tcp://127.0.0.1:0');
249 $port = (int)substr(strrchr(stream_socket_get_name($quiet, false), ':'), 1);
250 $t = microtime(true);
251 $e = $catch(fn() => Tls::connect('127.0.0.1', $port, ['peer_name' => 'localhost'], 1));
252 $test->assertTrue($e instanceof TlsException);
253 $test->assertStringContains('タイムアウト', $e->getMessage());
254 $test->assertTrue(microtime(true) - $t < 5);
255 fclose($quiet);
256
257 $e = $catch(fn() => Tls::connect('127.0.0.1', $port, [], 1));
258 $test->assertStringContains('接続出来ません', $e->getMessage());
259 });
260
261 $test->it('サーバーはTLS 1.1以前と安全でないTLS 1.2のClientHelloを拒否するはず', function($test) use ($srv): void {
262 // 拡張の無いClientHelloを送って、アラートを読む
263 $alert = function(string $version, string $suites) use ($srv): string {
264 $c = stream_socket_client("tcp://127.0.0.1:{$srv->port}", $errno, $errstr, 5);
265 stream_set_timeout($c, 5);
266 $body = $version.random_bytes(32)."\x00".pack('n', strlen($suites)).$suites."\x01\x00";
267 $hs = "\x01".substr(pack('N', strlen($body)), 1).$body;
268 fwrite($c, "\x16\x03\x01".pack('n', strlen($hs)).$hs);
269 $res = (string)fread($c, 7);
270 fclose($c);
271 return $res;
272 };
273 // TLS 1.1:protocol_version
274 $test->assertSame("\x15\x03\x03\x00\x02\x02\x46", $alert("\x03\x02", "\xC0\x2F"));
275 // TLS 1.2でもsignature_algorithmsが無い(SHA-1):handshake_failure
276 $test->assertSame("\x15\x03\x03\x00\x02\x02\x28", $alert("\x03\x03", "\xC0\x2B"));
277 // CBC・RSAの鍵交換だけ:handshake_failure
278 $test->assertSame("\x15\x03\x03\x00\x02\x02\x28", $alert("\x03\x03", "\x00\x2F\x00\x35\xC0\x13"));
279 });
280
281 $test->it('STARTTLSの様に、既に繋がったソケットでTLSを開始出来るはず', function($test) use ($srv, $ec): void {
282 $c = stream_socket_client("tcp://127.0.0.1:{$srv->port}", $errno, $errstr, 5);
283 stream_set_timeout($c, 5);
284 $s = Tls::enable($c, ['peer_name' => 'localhost', 'cafile' => $ec['ca']]);
285 fwrite($s, "upgraded\n");
286 $test->assertSame("upgraded\n", fgets($s));
287 fclose($s);
288 $test->assertFalse(is_resource($c));
289 });
290
291 $test->it('TLS 1.2の全部の暗号スイートで繋がるはず', function($test) use ($srv, $srvRsa, $open, $info, $catch): void {
292 foreach (Connection::SUITES12 as $id => [, , , $name, $keyType]) {
293 $s = $open($keyType === 'ec' ? $srv : $srvRsa, ['max_version' => '1.2', 'ciphers' => [$id]]);
294 $i = $info($s);
295 $test->assertSame('TLSv1.2', $i['version'], $name);
296 $test->assertSame($name, $i['cipher']);
297 $test->assertSame($name, Tls::info($s)['cipher']);
298 fwrite($s, "BIG 40000\n");
299 $got = '';
300 while (strlen($got) < 40000 && !feof($s)) $got .= fread($s, 65536);
301 $test->assertSame(40000, strlen($got), $name);
302 fclose($s);
303 }
304 foreach (array_keys(Connection::GROUPS) as $g) {
305 $s = $open($srv, ['max_version' => '1.2', 'groups' => [$g]]);
306 $test->assertSame($g, $info($s)['group'], $g);
307 fclose($s);
308 }
309 $s = $open($srv, ['max_version' => '1.2', 'alpn' => ['http/1.1']]);
310 $test->assertSame('http/1.1', $info($s)['alpn']);
311 fclose($s);
312
313 // サーバーの鍵(EC)に合う暗号スイートが無い
314 $e = $catch(fn() => $open($srv, ['max_version' => '1.2', 'ciphers' => [0xC02F, 0xCCA8]]));
315 $test->assertTrue($e instanceof TlsException);
316 $test->assertSame(Connection::ALERT_HANDSHAKE_FAILURE, $e->alert);
317 });
318
319 $test->it('送ったデータが1つのレコードに入っても、全部届くはず(両方のバージョン)', function($test) use ($srv, $open): void {
320 foreach (['1.2', '1.3'] as $v) {
321 $s = $open($srv, ['max_version' => $v]);
322 fwrite($s, "一行目\n二行目\n三行目\n");
323 $test->assertSame("一行目\n", fgets($s), $v);
324 $test->assertSame("二行目\n", fgets($s), $v);
325 $test->assertSame("三行目\n", fgets($s), $v);
326 fclose($s);
327 }
328 });
329
330 $test->it('バージョンを選べるはず(min_version・max_version)', function($test) use ($srv, $ec, $open, $info, $catch): void {
331 $old = MockServer::start('tls-server.php', [$ec['cert'], $ec['key'], '', '', '', '1.2']);
332 $s = $open($old);
333 $test->assertSame('TLSv1.2', $info($s)['version']);
334 fclose($s);
335 $e = $catch(fn() => $open($old, ['min_version' => '1.3']));
336 $test->assertTrue($e instanceof TlsException);
337 $test->assertSame(Connection::ALERT_PROTOCOL_VERSION, $e->alert);
338 $old->stop();
339
340 $test->assertThrows(fn() => $open($srv, ['min_version' => '1.1']), \InvalidArgumentException::class);
341 $test->assertThrows(fn() => $open($srv, ['min_version' => '1.3', 'max_version' => '1.2']), \InvalidArgumentException::class);
342 });
343
344 $test->it('TLS 1.3を使えるのに1.2に下げられたら(ダウングレード)拒否するはず', function($test) use ($ec, $open, $info, $catch): void {
345 $mitm = MockServer::start('tls-server.php', [$ec['cert'], $ec['key'], '', '', 'downgrade']);
346 $e = $catch(fn() => $open($mitm));
347 $test->assertTrue($e instanceof TlsException);
348 $test->assertStringContains('ダウングレード', $e->getMessage());
349 $test->assertSame(Connection::ALERT_ILLEGAL_PARAMETER, $e->alert);
350 // TLS 1.2だけを求めたなら、印があっても良い
351 $s = $open($mitm, ['max_version' => '1.2']);
352 $test->assertSame('TLSv1.2', $info($s)['version']);
353 fclose($s);
354 $mitm->stop();
355 });
356
357 $test->it('TLS 1.2の再ネゴシエーションを断って、続けられるはず', function($test) use ($srv, $open, $catch): void {
358 $s = $open($srv, ['max_version' => '1.2']);
359 fwrite($s, "HELLOREQ\n");
360 $test->assertSame("asked\n", fgets($s));
361 fwrite($s, "まだ使える\n");
362 $test->assertSame("まだ使える\n", fgets($s));
363 $test->assertThrows(fn() => Stream::connection($s)->keyUpdate(), \LogicException::class);
364 fclose($s);
365 });
366
367 $test->it('アラートの名前が分かるはず', function($test): void {
368 $test->assertSame('bad_record_mac', Connection::alertName(20));
369 $test->assertSame('protocol_version', Connection::alertName(70));
370 $test->assertSame('不明', Connection::alertName(255));
371 });
372
373 $test->it('間違ったFinished・署名を送るサーバーを拒否するはず(両方のバージョン)', function($test) use ($ec, $catch): void {
374 $cases = [
375 ['bad-finished', '1.3', 'Finishedが不正'],
376 ['bad-cv', '1.3', 'CertificateVerifyの署名が不正'],
377 ['bad-finished', '1.2', 'Finishedが不正'],
378 ['bad-cv', '1.2', 'ServerKeyExchangeの署名が不正'],
379 ];
380 foreach ($cases as [$mode, $v, $msg]) {
381 $evil = MockServer::start('tls-server.php', [$ec['cert'], $ec['key'], '', '', $mode]);
382 $e = $catch(fn() => Tls::connect('127.0.0.1', $evil->port, ['peer_name' => 'localhost', 'cafile' => $ec['ca'], 'max_version' => $v], 10));
383 $evil->stop();
384 $test->assertTrue($e instanceof TlsException, "{$mode} {$v}");
385 $test->assertStringContains($msg, $e->getMessage());
386 $test->assertSame(Connection::ALERT_DECRYPT_ERROR, $e->alert);
387 }
388 });
389
390 foreach ([$srv, $srvRsa, $srvP384, $srvSelf] as $s) $s->stop();
391});
392
393$test->describe('TLS 1.3・1.2(OpenSSLとの記録を再現する)', function($test) use ($catch): void {
394 if (!in_array('lbreplay', stream_get_wrappers(), true)) stream_wrapper_register('lbreplay', LibTlsReplay::class);
395 $pki = __DIR__.'/Helper/pki/';
396
397 // 記録と同じ乱数・鍵・時間でハンドシェイクする
398 $replay = function(string $name, ?callable $tamper = null, array $extra = []) use ($pki): array {
399 $rec = json_decode((string)file_get_contents($pki."tls-{$name}.json"), true);
400 $in = hex2bin($rec['s2c']);
401 LibTlsReplay::$in = $tamper ? $tamper($in) : $in;
402 LibTlsReplay::$out = '';
403 $opt = $extra + [
404 'peer_name' => 'www.example.com',
405 'cafile' => $pki.'root.pem',
406 '_time' => $rec['time'],
407 '_random' => hex2bin($rec['random']),
408 '_session_id' => hex2bin($rec['session_id']),
409 '_keys' => [
410 'x25519' => hex2bin($rec['x25519']),
411 'P-256' => (string)file_get_contents($pki.'p256.key'),
412 'P-384' => (string)file_get_contents($pki.'p384.key'),
413 ],
414 ];
415 $s = Tls::enable(fopen('lbreplay://x', 'r+'), $opt);
416 $replies = [];
417 foreach ($rec['lines'] as $l) {
418 fwrite($s, $l."\n");
419 $replies[] = bin2hex(rtrim((string)fgets($s), "\n"));
420 }
421 $info = Tls::info($s);
422 $error = Tls::error($s);
423 fclose($s);
424 return [$rec, $info, $replies, $error];
425 };
426
427 // レコードの位置
428 $records = function(string $data): array {
429 $out = [];
430 for ($p = 0; $p + 5 <= strlen($data); $p += 5 + unpack('n', $data, $p + 3)[1]) $out[] = [$p, ord($data[$p])];
431 return $out;
432 };
433
434 $test->it('OpenSSLのサーバーとの通信を同じバイトで再現するはず(AES-128-GCM、x25519)', function($test) use ($replay): void {
435 [$rec, $info, $replies] = $replay('aes');
436 $test->assertSame($rec['cipher'], $info['cipher']);
437 $test->assertSame('x25519', $info['group']);
438 $test->assertSame($rec['replies_hex'], $replies);
439 $test->assertSame($rec['c2s'], bin2hex(LibTlsReplay::$out));
440 });
441
442 $test->it('HelloRetryRequestの後の通信も再現するはず(ChaCha20-Poly1305、P-256)', function($test) use ($replay): void {
443 [$rec, $info, $replies] = $replay('hrr');
444 $test->assertSame('TLS_CHACHA20_POLY1305_SHA256', $info['cipher']);
445 $test->assertSame('P-256', $info['group']);
446 $test->assertSame($rec['replies_hex'], $replies);
447 $test->assertSame($rec['c2s'], bin2hex(LibTlsReplay::$out));
448 });
449
450 $test->it('改ざんされたハンドシェイク・データを見つけるはず', function($test) use ($replay, $records, $catch): void {
451 // 暗号化されたハンドシェイク(ServerHello・ChangeCipherSpecの次のレコード)
452 $e = $catch(fn() => $replay('aes', function(string $in) use ($records): string {
453 $r = $records($in);
454 $in[$r[2][0] + 20] = chr(ord($in[$r[2][0] + 20]) ^ 1);
455 return $in;
456 }));
457 $test->assertTrue($e instanceof TlsException);
458 $test->assertSame(Connection::ALERT_BAD_RECORD_MAC, $e->alert);
459
460 // 最後の返事
461 [$rec, , $replies, $error] = $replay('aes', function(string $in) use ($records): string {
462 $r = $records($in);
463 $last = end($r)[0];
464 $in[$last + 10] = chr(ord($in[$last + 10]) ^ 1);
465 return $in;
466 });
467 $test->assertSame(array_slice($rec['replies_hex'], 0, 2), array_slice($replies, 0, 2));
468 $test->assertSame('', $replies[2]);
469 $test->assertSame(Connection::ALERT_BAD_RECORD_MAC, $error?->alert);
470 });
471
472 $test->it('TLS 1.2の通信も同じバイトで再現するはず(PKCS#1・EMSなし、ECDSA・P-384、RSA-PSS)', function($test) use ($replay): void {
473 $expect = [
474 '12-rsa-pkcs1' => ['TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', 'x25519'],
475 '12-ecdsa' => ['TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256', 'P-384'],
476 '12-rsa-pss' => ['TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256', 'x25519'],
477 ];
478 foreach ($expect as $name => [$cipher, $group]) {
479 [$rec, $info, $replies] = $replay($name);
480 $test->assertSame('TLSv1.2', $info['version'], $name);
481 $test->assertSame($cipher, $info['cipher'], $name);
482 $test->assertSame($group, $info['group'], $name);
483 $test->assertSame($rec['replies_hex'], $replies, $name);
484 $test->assertSame($rec['c2s'], bin2hex(LibTlsReplay::$out), $name);
485 }
486 });
487
488 $test->it('TLS 1.2の改ざん(ServerKeyExchangeの署名、データ)を見つけるはず', function($test) use ($replay, $records, $catch): void {
489 // ServerKeyExchangeは暗号化されていない:x25519の点(32バイト)の後の署名を壊す
490 $e = $catch(fn() => $replay('12-rsa-pss', function(string $in): string {
491 $p = strpos($in, "\x03\x00\x1d\x20");
492 $in[$p + 4 + 32 + 4 + 10] = chr(ord($in[$p + 4 + 32 + 4 + 10]) ^ 1);
493 return $in;
494 }));
495 $test->assertTrue($e instanceof TlsException);
496 $test->assertStringContains('ServerKeyExchangeの署名が不正', $e->getMessage());
497
498 // 送っていないグループ(x25519をsecp521r1に書き換える)
499 $e = $catch(fn() => $replay('12-rsa-pss', function(string $in): string {
500 $p = strpos($in, "\x03\x00\x1d\x20");
501 $in[$p + 2] = "\x19";
502 return $in;
503 }));
504 $test->assertStringContains('送っていない鍵交換のグループ', $e?->getMessage() ?? '');
505 $test->assertSame(Connection::ALERT_ILLEGAL_PARAMETER, $e->alert);
506
507 // RSAの証明書なのにECDSAの暗号スイート(ServerHelloのC02FをC02Bに書き換える)
508 $e = $catch(fn() => $replay('12-rsa-pss', function(string $in): string {
509 $sh = 5 + 4 + 2 + 32;
510 $p = $sh + 1 + ord($in[$sh]);
511 $in[$p + 1] = "\x2B";
512 return $in;
513 }));
514 $test->assertStringContains('証明書の鍵が暗号スイートと合いません', $e?->getMessage() ?? '');
515
516 // 早過ぎるChangeCipherSpec(ServerHelloの直後、CVE-2014-0224の様な攻撃)
517 $e = $catch(fn() => $replay('12-rsa-pss', function(string $in) use ($records): string {
518 $r = $records($in);
519 $first = 5 + unpack('n', $in, 3)[1];
520 return substr($in, 0, $first)."\x14\x03\x03\x00\x01\x01".substr($in, $first);
521 }));
522 $test->assertStringContains('想定外のChangeCipherSpec', $e?->getMessage() ?? '');
523
524 // ChangeCipherSpecと暗号化されたFinishedを、暗号化されていないFinishedに替える
525 $e = $catch(fn() => $replay('12-rsa-pss', function(string $in) use ($records): string {
526 $r = $records($in);
527 foreach ($r as $i => [$pos, $type]) {
528 if ($type !== 20) continue;
529 $end = $r[$i + 2][0] ?? strlen($in);
530 return substr($in, 0, $pos)."\x16\x03\x03\x00\x10\x14\x00\x00\x0c".str_repeat("\0", 12).substr($in, $end);
531 }
532 return $in;
533 }));
534 $test->assertStringContains('Finishedを期待しました', $e?->getMessage() ?? '');
535
536 // 最後の返事
537 [$rec, , $replies, $error] = $replay('12-ecdsa', function(string $in) use ($records): string {
538 $r = $records($in);
539 $last = end($r)[0];
540 $in[$last + 10] = chr(ord($in[$last + 10]) ^ 1);
541 return $in;
542 });
543 $test->assertSame('', $replies[2]);
544 $test->assertSame(Connection::ALERT_BAD_RECORD_MAC, $error?->alert);
545 });
546
547 $test->it('記録の証明書の有効期限・CAを確認するはず', function($test) use ($replay, $catch): void {
548 $e = $catch(fn() => $replay('aes', null, ['_time' => time() + 86400 * 365 * 5]));
549 $test->assertSame(Connection::ALERT_CERTIFICATE_EXPIRED, $e?->alert);
550 // ホスト名はSNIに入る(ClientHelloが変わる)ので、信頼するCAを変える
551 $e = $catch(fn() => $replay('aes', null, ['cafile' => TestCert::get('self')['cert']]));
552 $test->assertSame(Connection::ALERT_UNKNOWN_CA, $e?->alert);
553 });
554});
555
556$test->printSummary();