| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, 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 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Lib\Tls; |
| 39 | |
| 40 | use Std\Lib\Crypto\Aes; |
| 41 | use Std\Lib\Crypto\ChaCha20; |
| 42 | use Std\Lib\Crypto\Ec; |
| 43 | use Std\Lib\Crypto\Rsa; |
| 44 | use Std\Lib\Crypto\X25519; |
| 45 | use 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 | */ |
| 58 | class 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 | } |