TLSライブラリ

守矢諏訪子 <suwako@076.moe>、2026-11-12 13:56
コミット b5a37f2beedd853f77455345ad2a56c674da1a8d ・親 33021bf ・ファイルを見る

60個のファイル、+6182 -156

変更 README-JP.md
@@ -222,8 +222,20 @@
222222 * 最初のクエリで接続します。パラメータ付きのクエリはプリペアドステートメントで実行するので、値をエスケープする必要はありません。
223223 * 整数の列は `int`、`FLOAT`・`DOUBLE` は `float`、`NULL` は `null`、その他(`DECIMAL`、日付、`BLOB` 等)は文字列で返します。
224224 * エラーは `Std\Lib\Db\DbException`(`getCode()` はサーバーのエラーコード、例:重複は1062、`sqlState` は SQLSTATE)を投げ、`log/db_log.txt` に書きます。
225-* 認証:`mysql_native_password`、`caching_sha2_password`(MySQL 8 の既定)と `sha256_password`(TLS か UNIX ソケットでなければ `php_openssl` が必要)、`mysql_clear_password`(TLS か UNIX ソケットのみ)。MariaDB の `client_ed25519`、圧縮、`LOAD DATA LOCAL INFILE` は未対応です。
225+* 認証:`mysql_native_password`、`caching_sha2_password`(MySQL 8 の既定)と `sha256_password`(TLS でない TCP では、パスワードをサーバーの RSA の鍵で暗号化する)、`mysql_clear_password`(TLS か UNIX ソケットのみ)。MariaDB の `client_ed25519`、圧縮、`LOAD DATA LOCAL INFILE` は未対応です。
226226
227+### TLS 1.3・1.2(php_openssl は不要)
228+HTTPS のリクエスト(`Curl`、ActivityPub)、SMTP の STARTTLS(`Mailer`)、MySQL の TLS は PHP だけで書かれた TLS 1.3・1.2 のクライアント `Std\Lib\Tls`(`php_gmp` が必要)を使うので、`php_openssl` は不要です。RSA の鍵と署名(ActivityPub)は `Std\Lib\Crypto\Rsa` を使い、今までの PEM の鍵もそのまま使えます。
229+* TLS 1.3 を優先し、TLS 1.2 にしか対応していないサーバーとは安全な暗号スイートだけで TLS 1.2 を使います(ECDHE と AES-GCM 又は ChaCha20-Poly1305、サーバーが対応していれば Extended Master Secret、再ネゴシエーションはしない)。CBC、RSA の鍵交換、DHE、TLS 1.1 以前は未対応です。TLS 1.3 に対応しているサーバーとの通信を攻撃者が TLS 1.2 に下げた場合は分かります。TLS 1.2 を拒否するには、オプションの `'min_version' => '1.3'` を使って下さい。
230+* 証明書は `config/config.php` の `TLS_CAFILE`(信頼する CA の PEM ファイル)で確認します。空なら、普通のシステムの場所を探します(OpenBSD は `/etc/ssl/cert.pem`)。httpd・php-fpm の chroot の中では `/var/www/etc/ssl/cert.pem` になるので、システムのファイルをそこにコピーして下さい。Windows にはこのファイルが無いので、`TLS_CAFILE` を設定して下さい(例:curl の `cacert.pem`)。
231+* 暗号スイート:TLS 1.3 は `TLS_CHACHA20_POLY1305_SHA256`、`TLS_AES_128_GCM_SHA256`、`TLS_AES_256_GCM_SHA384`、TLS 1.2 は `TLS_ECDHE_{ECDSA,RSA}_WITH_{CHACHA20_POLY1305_SHA256,AES_128_GCM_SHA256,AES_256_GCM_SHA384}`。鍵交換:x25519、secp256r1、secp384r1。署名:RSA-PSS、ECDSA(P-256、P-384、P-521)、RSA PKCS#1 v1.5(証明書と TLS 1.2 のみ)。セッションの再開、0-RTT、クライアント証明書、失効の確認は未対応です。
232+* 暗号化は PHP で計算するので、大きいファイルのダウンロードは openssl より遅いです(数 MB/秒)。
233+```php
234+$s = \Std\Lib\Tls::connect('example.com', 443);
235+fwrite($s, "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
236+echo stream_get_contents($s);
237+```
238+
227239 ### コメント
228240 `config/config.php` で `COMMENTS_ENABLED` を `true` にすると、全部のブログ記事の下にコメント欄が表示されます(書き込みには `AUTH_ENABLED` も必要)。
229241 * ログイン中のユーザーはコメント、返信、いいねが出来ます(Markdown:太字、斜体、取り消し線、コード、引用、リスト、リンク)。自分のコメントは削除出来、管理者とスタッフは全部のコメントを削除出来ます。JavaScript は不要です。
@@ -269,7 +281,7 @@
269281 アップロード出来るのは画像(PNG・JPEG・GIF・WebP、20 MiB まで)と動画(MP4・MKV・WebM、400 MiB まで)で、メタデータ(Exif、XMP、テキスト、MP4 の撮影日時・位置等)を取り除きます。動画は変換しないので、ブラウザで再生出来る形式(H.264 等)でアップロードして下さい。\
270282 大きいファイルを受け付ける様に、PHP の `upload_max_filesize` と `post_max_size`(添付4つ分)、httpd の `connection max request body`(既定は1 MiB、超えると「413 Payload Too Large」)も設定して下さい。動画を再生出来る様に、httpd の `types` に `include "/usr/share/misc/mime.types"` もお勧めします。
271283 他のサーバーのアイコンや画像は `/microblog/media` 経由で表示するので、CSP の `img-src` は `'self'` のままで構いません(閲覧者の IP アドレスも相手のサーバーに渡りません)。画像は `data/ap/remote/media/` に1週間キャッシュします。\
272-ユーザーの鍵は初めて使う時に作ります。chroot(OpenBSD の httpd と php-fpm 等)では `openssl.cnf` が見えないので、`data/ap/openssl.cnf`(空)を使います。
284+ユーザーの鍵(RSA、2048ビット)は初めて使う時に作ります。
273285
274286 ### 二段階認証
275287 `config/config.php` で `AUTH_2FA_ENABLED` を `true` にすると、各ユーザーがプロフィールの編集の「二段階認証」から有効に出来ます。`false` なら誰も使えず、有効にしていたユーザーもパスワードだけでログインします。\
@@ -279,5 +291,4 @@
279291 ## 必要な PHP モジュール
280292 * php_fileinfo(OpenBSDでデフォルトで有効している)
281293 * php_gmp
282-* php_mbstring(OpenBSDでデフォルトで有効している)
283-* php_openssl(OpenBSDでデフォルトで有効している)
294+* php_mbstring(OpenBSDでデフォルトで有効している)
変更 README.md
@@ -220,7 +220,19 @@
220220 * The connection is opened on the first query. Queries with parameters use prepared statements, so values never need escaping.
221221 * Integer columns come back as `int`, `FLOAT`/`DOUBLE` as `float`, `NULL` as `null`, and everything else (including `DECIMAL`, dates and `BLOB`) as strings.
222222 * Errors throw `Std\Lib\Db\DbException` (`getCode()` is the server's error code, for example 1062 for a duplicate key, and `sqlState` the SQLSTATE) and are written to `log/db_log.txt`.
223-* Authentication: `mysql_native_password`, `caching_sha2_password` (the MySQL 8 default) and `sha256_password` (both need `php_openssl` unless TLS or a UNIX socket is used), and `mysql_clear_password` (TLS or UNIX socket only). MariaDB's `client_ed25519`, compression and `LOAD DATA LOCAL INFILE` are not supported.
223+* Authentication: `mysql_native_password`, `caching_sha2_password` (the MySQL 8 default) and `sha256_password` (over plain TCP the password is encrypted with the server's RSA key), and `mysql_clear_password` (TLS or UNIX socket only). MariaDB's `client_ed25519`, compression and `LOAD DATA LOCAL INFILE` are not supported.
224+
225+### TLS 1.3 and 1.2 (no php_openssl)
226+HTTPS requests (`Curl`, ActivityPub), SMTP STARTTLS (`Mailer`) and MySQL over TLS use `Std\Lib\Tls`, a TLS 1.3 and 1.2 client written in pure PHP (it needs `php_gmp`), so `php_openssl` is not needed. RSA keys and signatures (ActivityPub) use `Std\Lib\Crypto\Rsa`, and existing PEM keys keep working.
227+* TLS 1.3 is preferred. Servers that only speak TLS 1.2 get TLS 1.2 with secure cipher suites only (ECDHE with AES-GCM or ChaCha20-Poly1305, Extended Master Secret when the server supports it, no renegotiation); CBC, RSA key exchange and DHE are not supported, and neither is TLS 1.1 or older. A TLS 1.3 server that is pushed down to TLS 1.2 by an attacker is detected. Use the option `'min_version' => '1.3'` to refuse TLS 1.2.
228+* Certificates are checked against `TLS_CAFILE` in `config/config.php` (a PEM file of trusted CAs). When it is empty, the usual system locations are tried (`/etc/ssl/cert.pem` on OpenBSD). Inside the httpd/php-fpm chroot that path is `/var/www/etc/ssl/cert.pem`, so copy the system file there. Windows has no such file, so set `TLS_CAFILE` (for example to curl's `cacert.pem`).
229+* Cipher suites: TLS 1.3 `TLS_CHACHA20_POLY1305_SHA256`, `TLS_AES_128_GCM_SHA256`, `TLS_AES_256_GCM_SHA384`; TLS 1.2 `TLS_ECDHE_{ECDSA,RSA}_WITH_{CHACHA20_POLY1305_SHA256,AES_128_GCM_SHA256,AES_256_GCM_SHA384}`. Key exchange: x25519, secp256r1, secp384r1. Signatures: RSA-PSS, ECDSA (P-256, P-384, P-521) and RSA PKCS#1 v1.5 (certificates and TLS 1.2 only). Session resumption, 0-RTT, client certificates and revocation checks are not supported.
230+* Encryption runs in PHP, so large downloads are slower than with openssl (a few MB/s).
231+```php
232+$s = \Std\Lib\Tls::connect('example.com', 443);
233+fwrite($s, "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
234+echo stream_get_contents($s);
235+```
224236
225237 ### Comments
226238 Set `COMMENTS_ENABLED` to `true` in `config/config.php` to show a comment section under every blog post (posting needs `AUTH_ENABLED`).
@@ -40,6 +40,9 @@
4040 // 静的ファイル(画像・CSS等)の場所。パス(例:「/static」=public/static/)、又は別のサーバーのURL(例:「https://ass.example.com/static」)
4141 // URLの場合、アップロードされたファイルはpublic/の下の同じパス(例:public/static/)に保存するので、そのサーバーと同期して下さい
4242 define('ASS_DOMAIN', '/static');
43+// TLS(HTTPS・STARTTLS等)で信頼するCAの証明書のPEMファイル。空ならシステムの場所(/etc/ssl/cert.pem等)を探す
44+// chroot(OpenBSDのhttpd・php-fpm)の中では/var/www/etc/ssl/cert.pemが/etc/ssl/cert.pemになる。Windowsでは指定が必要
45+define('TLS_CAFILE', '');
4346 define('SITEINFO', [
4447 'title' => 'Little Beast',
4548 'description' => 'PHP Framework',
@@ -37,6 +37,7 @@
3737 */
3838 namespace Std\Lib;
3939
40+use Std\Lib\Crypto\Rsa;
4041 use Std\Lib\Curl;
4142 use Std\Lib\Fedi\Actor;
4243 use Std\Lib\Fedi\Fedi;
@@ -61,7 +62,7 @@
6162 private array|\Closure $posts = [];
6263 private string $https;
6364 /** 秘密鍵のキャッシュ(1回の実行で何度も読まない様に) */
64- private ?\OpenSSLAsymmetricKey $privKey = null;
65+ private ?Rsa $privKey = null;
6566
6667 /** 連合しない(会員限定等の)カテゴリー */
6768 private const RESTRICTED = ['memberonly', 'subscriberonly', 'staffonly', 'nintendodevonly'];
@@ -630,11 +631,15 @@
630631 // 鍵の持ち主と鍵のURLは同じサーバーである事
631632 if (strtolower((string)parse_url($owner, PHP_URL_HOST)) !== strtolower((string)parse_url($sig['keyId'], PHP_URL_HOST))) return null;
632633
633- $pub = openssl_pkey_get_public($keys['publicKeyPem'] ?? '');
634+ try {
635+ $pub = Rsa::fromPem((string)($keys['publicKeyPem'] ?? ''));
636+ } catch (\InvalidArgumentException $e) {
637+ return null;
638+ }
634639 $raw = base64_decode($sig['signature'], true);
635- if (!$pub || $raw === false) return null;
640+ if ($raw === false) return null;
636641
637- return openssl_verify(implode("\n", $lines), $raw, $pub, OPENSSL_ALGO_SHA256) === 1 ? $owner : null;
642+ return $pub->verify(implode("\n", $lines), $raw, 'sha256') ? $owner : null;
638643 }
639644
640645 // 機能性メソッド
@@ -765,12 +770,12 @@
765770 *
766771 * @param string $url 送信先のURL
767772 * @param string $body 送信する本文(GETは空)
768- * @param \OpenSSLAsymmetricKey $priv 秘密鍵
773+ * @param Rsa $priv 秘密鍵
769774 * @param string $method post又はget
770775 * @param string|null $keyId 鍵のID(既定はブログのアクターの鍵。マルチユーザーではユーザーの鍵)
771776 * @return array|null 「名前: 値」のヘッダー、署名に失敗した場合はnull
772777 */
773- public function signRequest(string $url, string $body, \OpenSSLAsymmetricKey $priv, string $method = 'post', ?string $keyId = null): ?array {
778+ public function signRequest(string $url, string $body, Rsa $priv, string $method = 'post', ?string $keyId = null): ?array {
774779 $p = parse_url($url);
775780 if (!$p || empty($p['host'])) return null;
776781 $method = strtolower($method);
@@ -791,8 +796,10 @@
791796 $lines[] = "digest: {$headers['Digest']}";
792797 }
793798
794- if (!openssl_sign(implode("\n", $lines), $signature, $priv, OPENSSL_ALGO_SHA256)) {
795- logger(LogType::ActivityPub, 'エラー:署名に失敗: '.openssl_error_string());
799+ try {
800+ $signature = $priv->sign(implode("\n", $lines), 'sha256');
801+ } catch (\Throwable $e) {
802+ logger(LogType::ActivityPub, 'エラー:署名に失敗: '.$e->getMessage());
796803 return null;
797804 }
798805
@@ -809,15 +816,19 @@
809816 * 秘密鍵を読み込む(1回だけ)
810817 *
811818 * @param bool $log 失敗をログに書くか
812- * @return \OpenSSLAsymmetricKey|null 失敗した場合はnull
819+ * @return Rsa|null 失敗した場合はnull
813820 */
814- private function privateKey(bool $log = true): ?\OpenSSLAsymmetricKey {
821+ private function privateKey(bool $log = true): ?Rsa {
815822 if ($this->privKey !== null) return $this->privKey;
816823
817824 $file = FEDIINFO['privkey'] ?? '';
818825 $pem = is_string($file) && is_readable($file) ? file_get_contents($file) : false;
819- $key = $pem === false ? false : openssl_pkey_get_private($pem);
820- if ($key === false) {
826+ try {
827+ $key = $pem === false ? null : Rsa::fromPem($pem);
828+ } catch (\InvalidArgumentException $e) {
829+ $key = null;
830+ }
831+ if ($key === null || !$key->isPrivate()) {
821832 if ($log) logger(LogType::ActivityPub, "エラー:秘密鍵「{$file}」の読込に失敗");
822833 return null;
823834 }
@@ -0,0 +1,274 @@
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\Crypto;
39+
40+/**
41+ * AES(128・192・256ビットの鍵)とGCM(AEAD)
42+ *
43+ * 例:
44+ * $aes = new Aes($key);
45+ * $sealed = $aes->seal($iv12, $plain, $aad); // 暗号文||タグ(16バイト)
46+ * $plain = $aes->open($iv12, $sealed, $aad); // 改ざんされていればnull
47+ */
48+class Aes {
49+ private static ?array $t = null;
50+
51+ private array $rk;
52+ private int $rounds;
53+ /** GHASHの表(4ビット毎、上位と下位の64ビット) */
54+ private array $hh = [];
55+ private array $hl = [];
56+
57+ // GHASHの4ビットの剰余(OpenSSLのrem_4bit)
58+ private const REM4 = [0x0000, 0x1C20, 0x3840, 0x2460, 0x7080, 0x6CA0, 0x48C0, 0x54E0,
59+ 0xE100, 0xFD20, 0xD940, 0xC560, 0x9180, 0x8DA0, 0xA9C0, 0xB5E0];
60+
61+ /**
62+ * @param string $key 16、24、32バイト
63+ */
64+ public function __construct(string $key) {
65+ $len = strlen($key);
66+ if (!in_array($len, [16, 24, 32], true)) throw new \InvalidArgumentException('AES:鍵は16、24、32バイト');
67+ self::tables();
68+ $this->expand($key);
69+
70+ // GHASHのH = E(K, 0)
71+ [$h0, $h1, $h2, $h3] = $this->encryptWords(0, 0, 0, 0);
72+ $vh = ($h0 << 32) | $h1;
73+ $vl = ($h2 << 32) | $h3;
74+ $hh = array_fill(0, 16, 0);
75+ $hl = array_fill(0, 16, 0);
76+ $hh[8] = $vh;
77+ $hl[8] = $vl;
78+ foreach ([4, 2, 1] as $i) {
79+ $t = (0xE1 << 56) & -($vl & 1);
80+ $vl = ($vh << 63) | (($vl >> 1) & PHP_INT_MAX);
81+ $vh = (($vh >> 1) & PHP_INT_MAX) ^ $t;
82+ $hh[$i] = $vh;
83+ $hl[$i] = $vl;
84+ }
85+ foreach ([2, 4, 8] as $i) {
86+ for ($j = 1; $j < $i; $j++) {
87+ $hh[$i + $j] = $hh[$i] ^ $hh[$j];
88+ $hl[$i + $j] = $hl[$i] ^ $hl[$j];
89+ }
90+ }
91+ $this->hh = $hh;
92+ $this->hl = $hl;
93+ }
94+
95+ /** 1ブロック(16バイト)を暗号化する */
96+ public function encryptBlock(string $block): string {
97+ if (strlen($block) !== 16) throw new \InvalidArgumentException('AES:ブロックは16バイト');
98+ $w = unpack('N4', $block);
99+ return pack('N4', ...$this->encryptWords($w[1], $w[2], $w[3], $w[4]));
100+ }
101+
102+ /**
103+ * GCMで暗号化する
104+ *
105+ * @param string $iv 12バイト
106+ * @param string $plain 平文
107+ * @param string $aad 追加の認証データ
108+ * @return string 暗号文||タグ(16バイト)
109+ */
110+ public function seal(string $iv, string $plain, string $aad = ''): string {
111+ if (strlen($iv) !== 12) throw new \InvalidArgumentException('AES-GCM:IVは12バイト');
112+ $ct = $this->ctr($iv, $plain);
113+ return $ct.$this->tag($iv, $ct, $aad);
114+ }
115+
116+ /**
117+ * GCMで復号する
118+ *
119+ * @param string $iv 12バイト
120+ * @param string $sealed 暗号文||タグ
121+ * @param string $aad 追加の認証データ
122+ * @return string|null 平文、改ざんされていればnull
123+ */
124+ public function open(string $iv, string $sealed, string $aad = ''): ?string {
125+ if (strlen($iv) !== 12 || strlen($sealed) < 16) return null;
126+ $ct = substr($sealed, 0, -16);
127+ if (!hash_equals($this->tag($iv, $ct, $aad), substr($sealed, -16))) return null;
128+ return $this->ctr($iv, $ct);
129+ }
130+
131+ ///////////////
132+
133+ /** カウンタモード(カウンタは2から) */
134+ private function ctr(string $iv, string $data): string {
135+ $len = strlen($data);
136+ if ($len === 0) return '';
137+ [, $n0, $n1, $n2] = unpack('N3', $iv);
138+ $ks = '';
139+ $blocks = intdiv($len + 15, 16);
140+ for ($i = 0; $i < $blocks; $i++) {
141+ $ks .= pack('N4', ...$this->encryptWords($n0, $n1, $n2, ($i + 2) & 0xFFFFFFFF));
142+ }
143+ return $data ^ substr($ks, 0, $len);
144+ }
145+
146+ /** タグ = E(K, J0) xor GHASH(aad, ct) */
147+ private function tag(string $iv, string $ct, string $aad): string {
148+ $zh = 0;
149+ $zl = 0;
150+ foreach ([$aad, $ct] as $d) {
151+ $len = strlen($d);
152+ if ($len % 16) $d .= str_repeat("\0", 16 - $len % 16);
153+ for ($i = 0, $n = strlen($d); $i < $n; $i += 16) {
154+ $b = unpack('J2', $d, $i);
155+ [$zh, $zl] = $this->gmult($zh ^ $b[1], $zl ^ $b[2]);
156+ }
157+ }
158+ [$zh, $zl] = $this->gmult($zh ^ (strlen($aad) * 8), $zl ^ (strlen($ct) * 8));
159+
160+ [, $n0, $n1, $n2] = unpack('N3', $iv);
161+ $ek = pack('N4', ...$this->encryptWords($n0, $n1, $n2, 1));
162+ return $ek ^ pack('J2', $zh, $zl);
163+ }
164+
165+ /** X * H(GF(2^128)、OpenSSLのgcm_gmult_4bit) */
166+ private function gmult(int $xh, int $xl): array {
167+ $H = $this->hh;
168+ $L = $this->hl;
169+ $mask = 0x0FFFFFFFFFFFFFFF;
170+
171+ $b = $xl & 0xFF;
172+ $nhi = $b >> 4;
173+ $zh = $H[$b & 0xF];
174+ $zl = $L[$b & 0xF];
175+
176+ for ($cnt = 15; ; ) {
177+ $rem = $zl & 0xF;
178+ $zl = ($zh << 60) | (($zl >> 4) & $mask);
179+ $zh = (($zh >> 4) & $mask) ^ (self::REM4[$rem] << 48) ^ $H[$nhi];
180+ $zl ^= $L[$nhi];
181+ if (--$cnt < 0) break;
182+
183+ $b = $cnt >= 8 ? ($xl >> (8 * (15 - $cnt))) & 0xFF : ($xh >> (8 * (7 - $cnt))) & 0xFF;
184+ $nhi = $b >> 4;
185+ $nlo = $b & 0xF;
186+ $rem = $zl & 0xF;
187+ $zl = ($zh << 60) | (($zl >> 4) & $mask);
188+ $zh = (($zh >> 4) & $mask) ^ (self::REM4[$rem] << 48) ^ $H[$nlo];
189+ $zl ^= $L[$nlo];
190+ }
191+ return [$zh, $zl];
192+ }
193+
194+ /** 4つの32ビットの語を暗号化する */
195+ private function encryptWords(int $s0, int $s1, int $s2, int $s3): array {
196+ [$t0, $t1, $t2, $t3, $sb] = self::$t;
197+ $rk = $this->rk;
198+ $s0 ^= $rk[0];
199+ $s1 ^= $rk[1];
200+ $s2 ^= $rk[2];
201+ $s3 ^= $rk[3];
202+ $k = 4;
203+ for ($r = 1; $r < $this->rounds; $r++) {
204+ $u0 = $t0[$s0 >> 24] ^ $t1[($s1 >> 16) & 0xFF] ^ $t2[($s2 >> 8) & 0xFF] ^ $t3[$s3 & 0xFF] ^ $rk[$k];
205+ $u1 = $t0[$s1 >> 24] ^ $t1[($s2 >> 16) & 0xFF] ^ $t2[($s3 >> 8) & 0xFF] ^ $t3[$s0 & 0xFF] ^ $rk[$k + 1];
206+ $u2 = $t0[$s2 >> 24] ^ $t1[($s3 >> 16) & 0xFF] ^ $t2[($s0 >> 8) & 0xFF] ^ $t3[$s1 & 0xFF] ^ $rk[$k + 2];
207+ $s3 = $t0[$s3 >> 24] ^ $t1[($s0 >> 16) & 0xFF] ^ $t2[($s1 >> 8) & 0xFF] ^ $t3[$s2 & 0xFF] ^ $rk[$k + 3];
208+ $s0 = $u0;
209+ $s1 = $u1;
210+ $s2 = $u2;
211+ $k += 4;
212+ }
213+ return [
214+ (($sb[$s0 >> 24] << 24) | ($sb[($s1 >> 16) & 0xFF] << 16) | ($sb[($s2 >> 8) & 0xFF] << 8) | $sb[$s3 & 0xFF]) ^ $rk[$k],
215+ (($sb[$s1 >> 24] << 24) | ($sb[($s2 >> 16) & 0xFF] << 16) | ($sb[($s3 >> 8) & 0xFF] << 8) | $sb[$s0 & 0xFF]) ^ $rk[$k + 1],
216+ (($sb[$s2 >> 24] << 24) | ($sb[($s3 >> 16) & 0xFF] << 16) | ($sb[($s0 >> 8) & 0xFF] << 8) | $sb[$s1 & 0xFF]) ^ $rk[$k + 2],
217+ (($sb[$s3 >> 24] << 24) | ($sb[($s0 >> 16) & 0xFF] << 16) | ($sb[($s1 >> 8) & 0xFF] << 8) | $sb[$s2 & 0xFF]) ^ $rk[$k + 3],
218+ ];
219+ }
220+
221+ /** 鍵の拡大(FIPS 197 5.2) */
222+ private function expand(string $key): void {
223+ $sb = self::$t[4];
224+ $nk = intdiv(strlen($key), 4);
225+ $this->rounds = $nk + 6;
226+ $w = array_values(unpack('N*', $key));
227+ $rcon = 1;
228+ for ($i = $nk; $i < 4 * ($this->rounds + 1); $i++) {
229+ $t = $w[$i - 1];
230+ if ($i % $nk === 0) {
231+ $t = (($t << 8) & 0xFFFFFFFF) | ($t >> 24);
232+ $t = ($sb[$t >> 24] << 24) | ($sb[($t >> 16) & 0xFF] << 16) | ($sb[($t >> 8) & 0xFF] << 8) | $sb[$t & 0xFF];
233+ $t ^= $rcon << 24;
234+ $rcon = (($rcon << 1) ^ (($rcon & 0x80) ? 0x1B : 0)) & 0xFF;
235+ } else if ($nk > 6 && $i % $nk === 4) {
236+ $t = ($sb[$t >> 24] << 24) | ($sb[($t >> 16) & 0xFF] << 16) | ($sb[($t >> 8) & 0xFF] << 8) | $sb[$t & 0xFF];
237+ }
238+ $w[$i] = $w[$i - $nk] ^ $t;
239+ }
240+ $this->rk = $w;
241+ }
242+
243+ /** S-boxとTテーブル(初めて使う時に作る) */
244+ private static function tables(): void {
245+ if (self::$t !== null) return;
246+ $sb = array_fill(0, 256, 0);
247+ $p = 1;
248+ $q = 1;
249+ do {
250+ $p = ($p ^ ($p << 1) ^ (($p & 0x80) ? 0x1B : 0)) & 0xFF;
251+ $q ^= $q << 1;
252+ $q ^= $q << 2;
253+ $q ^= $q << 4;
254+ $q &= 0xFF;
255+ if ($q & 0x80) $q ^= 0x09;
256+ $x = $q ^ (($q << 1) | ($q >> 7)) ^ (($q << 2) | ($q >> 6)) ^ (($q << 3) | ($q >> 5)) ^ (($q << 4) | ($q >> 4));
257+ $sb[$p] = ($x ^ 0x63) & 0xFF;
258+ } while ($p !== 1);
259+ $sb[0] = 0x63;
260+
261+ $t0 = $t1 = $t2 = $t3 = [];
262+ for ($i = 0; $i < 256; $i++) {
263+ $s = $sb[$i];
264+ $s2 = (($s << 1) ^ (($s & 0x80) ? 0x1B : 0)) & 0xFF;
265+ $s3 = $s2 ^ $s;
266+ $v = ($s2 << 24) | ($s << 16) | ($s << 8) | $s3;
267+ $t0[$i] = $v;
268+ $t1[$i] = (($v >> 8) | ($v << 24)) & 0xFFFFFFFF;
269+ $t2[$i] = (($v >> 16) | ($v << 16)) & 0xFFFFFFFF;
270+ $t3[$i] = (($v >> 24) | ($v << 8)) & 0xFFFFFFFF;
271+ }
272+ self::$t = [$t0, $t1, $t2, $t3, $sb];
273+ }
274+}
@@ -0,0 +1,324 @@
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\Crypto;
39+
40+/**
41+ * ASN.1(DER)の読み書きとPEM
42+ *
43+ * 読む:
44+ * $node = Asn1::parse($der); // ['tag' => 0x30, 'value' => 中身, 'raw' => タグ・長さ・中身]
45+ * foreach (Asn1::children($node) as $c) … // 構造型の中の要素
46+ * 書く:
47+ * $der = Asn1::seq(Asn1::int(1), Asn1::oid('1.2.840.113549.1.1.1'), Asn1::null());
48+ */
49+class Asn1 {
50+ public const BOOLEAN = 0x01;
51+ public const INTEGER = 0x02;
52+ public const BIT_STRING = 0x03;
53+ public const OCTET_STRING = 0x04;
54+ public const NULL = 0x05;
55+ public const OID = 0x06;
56+ public const UTF8_STRING = 0x0C;
57+ public const PRINTABLE_STRING = 0x13;
58+ public const T61_STRING = 0x14;
59+ public const IA5_STRING = 0x16;
60+ public const UTC_TIME = 0x17;
61+ public const GENERALIZED_TIME = 0x18;
62+ public const UNIVERSAL_STRING = 0x1C;
63+ public const BMP_STRING = 0x1E;
64+ public const SEQUENCE = 0x30;
65+ public const SET = 0x31;
66+
67+ private const MAX_DEPTH = 32;
68+
69+ /**
70+ * DERの要素を1つ読む
71+ *
72+ * @param string $der データ
73+ * @param int $pos 位置(読んだ分だけ進める)
74+ * @return array tag(最初のバイト)、value(中身)、raw(全体)
75+ * @throws \UnexpectedValueException 壊れたDERの場合
76+ */
77+ public static function read(string $der, int &$pos): array {
78+ $len = strlen($der);
79+ if ($pos + 2 > $len) throw new \UnexpectedValueException('ASN.1:データが足りない');
80+
81+ $start = $pos;
82+ $tag = ord($der[$pos++]);
83+ if (($tag & 0x1F) === 0x1F) throw new \UnexpectedValueException('ASN.1:長いタグは未対応');
84+
85+ $l = ord($der[$pos++]);
86+ if ($l & 0x80) {
87+ $n = $l & 0x7F;
88+ if ($n === 0 || $n > 4 || $pos + $n > $len) throw new \UnexpectedValueException('ASN.1:不正な長さ');
89+ $l = 0;
90+ for ($i = 0; $i < $n; $i++) $l = ($l << 8) | ord($der[$pos++]);
91+ }
92+ if ($pos + $l > $len) throw new \UnexpectedValueException('ASN.1:長さがデータを超える');
93+
94+ $value = substr($der, $pos, $l);
95+ $pos += $l;
96+ return ['tag' => $tag, 'value' => $value, 'raw' => substr($der, $start, $pos - $start)];
97+ }
98+
99+ /**
100+ * DER全体を1つの要素として読む(後ろに余計なデータがあれば例外)
101+ *
102+ * @param string $der
103+ * @return array
104+ * @throws \UnexpectedValueException
105+ */
106+ public static function parse(string $der): array {
107+ $pos = 0;
108+ $node = self::read($der, $pos);
109+ if ($pos !== strlen($der)) throw new \UnexpectedValueException('ASN.1:余計なデータ');
110+ return $node;
111+ }
112+
113+ /**
114+ * 構造型(SEQUENCE、SET、[n]等)の中の要素
115+ *
116+ * @param array $node
117+ * @return array 要素の配列
118+ * @throws \UnexpectedValueException
119+ */
120+ public static function children(array $node): array {
121+ if (!($node['tag'] & 0x20)) throw new \UnexpectedValueException('ASN.1:構造型ではない');
122+ $out = [];
123+ $pos = 0;
124+ $v = $node['value'];
125+ while ($pos < strlen($v)) $out[] = self::read($v, $pos);
126+ return $out;
127+ }
128+
129+ /**
130+ * 種類を確認して、構造型の中の要素を返す
131+ *
132+ * @param array $node
133+ * @param int $tag 期待するタグ
134+ * @param int $min 最低の要素の数
135+ * @return array
136+ * @throws \UnexpectedValueException
137+ */
138+ public static function expect(array $node, int $tag, int $min = 0): array {
139+ if ($node['tag'] !== $tag) throw new \UnexpectedValueException(sprintf('ASN.1:0x%02Xを期待したが0x%02X', $tag, $node['tag']));
140+ $c = self::children($node);
141+ if (count($c) < $min) throw new \UnexpectedValueException('ASN.1:要素が足りない');
142+ return $c;
143+ }
144+
145+ ///////////////
146+ // 値
147+
148+ /** INTEGERを読む(2の補数) */
149+ public static function toInt(array $node): \GMP {
150+ if ($node['tag'] !== self::INTEGER || $node['value'] === '') throw new \UnexpectedValueException('ASN.1:INTEGERではない');
151+ $v = $node['value'];
152+ $n = gmp_import($v);
153+ if (ord($v[0]) & 0x80) $n = gmp_sub($n, gmp_pow(2, 8 * strlen($v)));
154+ return $n;
155+ }
156+
157+ /** OBJECT IDENTIFIERを読む(「1.2.840.113549」の形) */
158+ public static function toOid(array $node): string {
159+ if ($node['tag'] !== self::OID || $node['value'] === '') throw new \UnexpectedValueException('ASN.1:OIDではない');
160+ $parts = [];
161+ $n = 0;
162+ $v = $node['value'];
163+ for ($i = 0; $i < strlen($v); $i++) {
164+ $b = ord($v[$i]);
165+ if ($n > (PHP_INT_MAX >> 7)) throw new \UnexpectedValueException('ASN.1:OIDが大き過ぎる');
166+ $n = ($n << 7) | ($b & 0x7F);
167+ if (!($b & 0x80)) {
168+ if (empty($parts)) {
169+ $first = min(2, intdiv($n, 40));
170+ $parts[] = $first;
171+ $parts[] = $n - 40 * $first;
172+ } else {
173+ $parts[] = $n;
174+ }
175+ $n = 0;
176+ }
177+ }
178+ return implode('.', $parts);
179+ }
180+
181+ /** BIT STRINGの中身(未使用ビットの数のバイトを除く) */
182+ public static function toBits(array $node): string {
183+ if ($node['tag'] !== self::BIT_STRING || $node['value'] === '') throw new \UnexpectedValueException('ASN.1:BIT STRINGではない');
184+ return substr($node['value'], 1);
185+ }
186+
187+ /** 文字列の型をUTF-8にする */
188+ public static function toString(array $node): string {
189+ $v = $node['value'];
190+ return match ($node['tag']) {
191+ self::BMP_STRING => mb_convert_encoding($v, 'UTF-8', 'UTF-16BE'),
192+ self::UNIVERSAL_STRING => mb_convert_encoding($v, 'UTF-8', 'UTF-32BE'),
193+ self::T61_STRING => mb_check_encoding($v, 'UTF-8') ? $v : mb_convert_encoding($v, 'UTF-8', 'ISO-8859-1'),
194+ default => $v,
195+ };
196+ }
197+
198+ /** UTCTime・GeneralizedTimeをUNIX時間にする */
199+ public static function toTime(array $node): int {
200+ $v = $node['value'];
201+ if ($node['tag'] === self::UTC_TIME && preg_match('/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/', $v, $m)) {
202+ $y = (int)$m[1];
203+ $y += $y < 50 ? 2000 : 1900;
204+ } else if ($node['tag'] === self::GENERALIZED_TIME && preg_match('/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\.\d+)?Z$/', $v, $m)) {
205+ $y = (int)$m[1];
206+ } else {
207+ throw new \UnexpectedValueException('ASN.1:未対応の時間');
208+ }
209+ return gmmktime((int)$m[4], (int)$m[5], (int)$m[6], (int)$m[2], (int)$m[3], $y);
210+ }
211+
212+ ///////////////
213+ // 書く
214+
215+ /** タグ・長さ・中身 */
216+ public static function tlv(int $tag, string $value): string {
217+ $l = strlen($value);
218+ if ($l < 0x80) return chr($tag).chr($l).$value;
219+ $b = ltrim(pack('N', $l), "\0");
220+ return chr($tag).chr(0x80 | strlen($b)).$b.$value;
221+ }
222+
223+ public static function seq(string ...$items): string {
224+ return self::tlv(self::SEQUENCE, implode('', $items));
225+ }
226+
227+ public static function set(string ...$items): string {
228+ return self::tlv(self::SET, implode('', $items));
229+ }
230+
231+ /** INTEGER(負数も) */
232+ public static function int(\GMP|int $n): string {
233+ $n = $n instanceof \GMP ? $n : gmp_init($n);
234+ if (gmp_sign($n) >= 0) {
235+ $b = gmp_export($n);
236+ if ($b === '' || (ord($b[0]) & 0x80)) $b = "\0".$b;
237+ return self::tlv(self::INTEGER, $b);
238+ }
239+ $len = strlen(gmp_export(gmp_abs($n))) + 1;
240+ $b = substr(str_pad(gmp_export(gmp_add(gmp_pow(2, 8 * $len), $n)), $len, "\0", STR_PAD_LEFT), -$len);
241+ while (strlen($b) > 1 && $b[0] === "\xFF" && (ord($b[1]) & 0x80)) $b = substr($b, 1);
242+ return self::tlv(self::INTEGER, $b);
243+ }
244+
245+ public static function oid(string $oid): string {
246+ $p = array_map('intval', explode('.', $oid));
247+ $out = '';
248+ $nums = array_merge([40 * $p[0] + $p[1]], array_slice($p, 2));
249+ foreach ($nums as $n) {
250+ $b = chr($n & 0x7F);
251+ for ($n >>= 7; $n > 0; $n >>= 7) $b = chr(0x80 | ($n & 0x7F)).$b;
252+ $out .= $b;
253+ }
254+ return self::tlv(self::OID, $out);
255+ }
256+
257+ public static function null(): string {
258+ return "\x05\x00";
259+ }
260+
261+ public static function bool(bool $v): string {
262+ return self::tlv(self::BOOLEAN, $v ? "\xFF" : "\x00");
263+ }
264+
265+ public static function bits(string $v): string {
266+ return self::tlv(self::BIT_STRING, "\0".$v);
267+ }
268+
269+ public static function octets(string $v): string {
270+ return self::tlv(self::OCTET_STRING, $v);
271+ }
272+
273+ public static function utf8(string $v): string {
274+ return self::tlv(self::UTF8_STRING, $v);
275+ }
276+
277+ /** 2050年までUTCTime、それ以降はGeneralizedTime(RFC 5280) */
278+ public static function time(int $t): string {
279+ $y = (int)gmdate('Y', $t);
280+ return $y < 2050 && $y >= 1950
281+ ? self::tlv(self::UTC_TIME, gmdate('ymdHis', $t).'Z')
282+ : self::tlv(self::GENERALIZED_TIME, gmdate('YmdHis', $t).'Z');
283+ }
284+
285+ /** 明示的なタグ[n] */
286+ public static function explicit(int $n, string $inner): string {
287+ return self::tlv(0xA0 | $n, $inner);
288+ }
289+
290+ ///////////////
291+ // PEM
292+
293+ /**
294+ * DERをPEMにする
295+ *
296+ * @param string $der
297+ * @param string $label 例:PRIVATE KEY、PUBLIC KEY、CERTIFICATE
298+ * @return string
299+ */
300+ public static function pem(string $der, string $label): string {
301+ return "-----BEGIN {$label}-----\n".chunk_split(base64_encode($der), 64, "\n")."-----END {$label}-----\n";
302+ }
303+
304+ /**
305+ * PEMのブロックを全部読む
306+ *
307+ * @param string $text PEM(複数のブロックも可)
308+ * @return array ['label' => …, 'der' => …]の配列
309+ */
310+ public static function unpem(string $text): array {
311+ preg_match_all('/-----BEGIN ([A-Z0-9 ]+)-----\s*(.*?)-----END \1-----/s', $text, $m, PREG_SET_ORDER);
312+ $out = [];
313+ foreach ($m as $b) {
314+ // 「Proc-Type:」等のヘッダーがある(暗号化された)PEM
315+ if (str_contains($b[2], ':')) {
316+ $out[] = ['label' => $b[1], 'der' => '', 'encrypted' => true];
317+ continue;
318+ }
319+ $der = base64_decode(preg_replace('/\s+/', '', $b[2]), true);
320+ if ($der !== false) $out[] = ['label' => $b[1], 'der' => $der];
321+ }
322+ return $out;
323+ }
324+}
@@ -0,0 +1,247 @@
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\Crypto;
39+
40+/**
41+ * ChaCha20-Poly1305(RFC 8439のAEAD)
42+ *
43+ * 例:
44+ * $c = new ChaCha20($key32);
45+ * $sealed = $c->seal($nonce12, $plain, $aad); // 暗号文||タグ(16バイト)
46+ * $plain = $c->open($nonce12, $sealed, $aad); // 改ざんされていればnull
47+ */
48+class ChaCha20 {
49+ private array $k;
50+
51+ /**
52+ * @param string $key 32バイト
53+ */
54+ public function __construct(string $key) {
55+ if (strlen($key) !== 32) throw new \InvalidArgumentException('ChaCha20:鍵は32バイト');
56+ $this->k = array_values(unpack('V8', $key));
57+ }
58+
59+ /**
60+ * 暗号化する(AEAD)
61+ *
62+ * @param string $nonce 12バイト
63+ * @param string $plain 平文
64+ * @param string $aad 追加の認証データ
65+ * @return string 暗号文||タグ(16バイト)
66+ */
67+ public function seal(string $nonce, string $plain, string $aad = ''): string {
68+ if (strlen($nonce) !== 12) throw new \InvalidArgumentException('ChaCha20:ノンスは12バイト');
69+ $ct = $this->xor($nonce, $plain, 1);
70+ return $ct.$this->mac($nonce, $ct, $aad);
71+ }
72+
73+ /**
74+ * 復号する(AEAD)
75+ *
76+ * @return string|null 平文、改ざんされていればnull
77+ */
78+ public function open(string $nonce, string $sealed, string $aad = ''): ?string {
79+ if (strlen($nonce) !== 12 || strlen($sealed) < 16) return null;
80+ $ct = substr($sealed, 0, -16);
81+ if (!hash_equals($this->mac($nonce, $ct, $aad), substr($sealed, -16))) return null;
82+ return $this->xor($nonce, $ct, 1);
83+ }
84+
85+ /**
86+ * 鍵ストリームとXORする(ChaCha20だけ)
87+ *
88+ * @param string $nonce 12バイト
89+ * @param string $data データ
90+ * @param int $counter 最初のブロックのカウンタ
91+ * @return string
92+ */
93+ public function xor(string $nonce, string $data, int $counter = 0): string {
94+ $len = strlen($data);
95+ if ($len === 0) return '';
96+ $n = unpack('V3', $nonce);
97+ $ks = '';
98+ for ($i = 0, $blocks = intdiv($len + 63, 64); $i < $blocks; $i++) {
99+ $ks .= $this->block(($counter + $i) & 0xFFFFFFFF, $n[1], $n[2], $n[3]);
100+ }
101+ return $data ^ substr($ks, 0, $len);
102+ }
103+
104+ /** Poly1305のタグ(鍵はカウンタ0のブロック) */
105+ private function mac(string $nonce, string $ct, string $aad): string {
106+ $n = unpack('V3', $nonce);
107+ $key = substr($this->block(0, $n[1], $n[2], $n[3]), 0, 32);
108+ $pad = fn(string $s): string => strlen($s) % 16 ? str_repeat("\0", 16 - strlen($s) % 16) : '';
109+ return self::poly1305($key, $aad.$pad($aad).$ct.$pad($ct).pack('P2', strlen($aad), strlen($ct)));
110+ }
111+
112+ /**
113+ * Poly1305(RFC 8439 2.5、26ビットの5つの整数で計算する)
114+ *
115+ * @param string $key 32バイト(r||s)
116+ * @param string $msg メッセージ
117+ * @return string 16バイト
118+ */
119+ public static function poly1305(string $key, string $msg): string {
120+ $m = 0x3FFFFFF;
121+ [, $t0, $t1, $t2, $t3] = unpack('V4', $key);
122+ $r0 = $t0 & 0x3FFFFFF;
123+ $r1 = (($t0 >> 26) | ($t1 << 6)) & 0x3FFFF03;
124+ $r2 = (($t1 >> 20) | ($t2 << 12)) & 0x3FFC0FF;
125+ $r3 = (($t2 >> 14) | ($t3 << 18)) & 0x3F03FFF;
126+ $r4 = ($t3 >> 8) & 0x00FFFFF;
127+ $s1 = $r1 * 5;
128+ $s2 = $r2 * 5;
129+ $s3 = $r3 * 5;
130+ $s4 = $r4 * 5;
131+ $h0 = $h1 = $h2 = $h3 = $h4 = 0;
132+
133+ $len = strlen($msg);
134+ $full = $len - $len % 16;
135+ for ($i = 0; $i < $len; $i += 16) {
136+ if ($i < $full) {
137+ [, $t0, $t1, $t2, $t3] = unpack('V4', $msg, $i);
138+ $hibit = 1 << 24;
139+ } else {
140+ // 最後の短いブロック:1バイトの1を足して、0で埋める
141+ [, $t0, $t1, $t2, $t3] = unpack('V4', str_pad(substr($msg, $i)."\x01", 16, "\0"));
142+ $hibit = 0;
143+ }
144+ $h0 += $t0 & $m;
145+ $h1 += (($t0 >> 26) | ($t1 << 6)) & $m;
146+ $h2 += (($t1 >> 20) | ($t2 << 12)) & $m;
147+ $h3 += (($t2 >> 14) | ($t3 << 18)) & $m;
148+ $h4 += ($t3 >> 8) | $hibit;
149+
150+ $d0 = $h0 * $r0 + $h1 * $s4 + $h2 * $s3 + $h3 * $s2 + $h4 * $s1;
151+ $d1 = $h0 * $r1 + $h1 * $r0 + $h2 * $s4 + $h3 * $s3 + $h4 * $s2;
152+ $d2 = $h0 * $r2 + $h1 * $r1 + $h2 * $r0 + $h3 * $s4 + $h4 * $s3;
153+ $d3 = $h0 * $r3 + $h1 * $r2 + $h2 * $r1 + $h3 * $r0 + $h4 * $s4;
154+ $d4 = $h0 * $r4 + $h1 * $r3 + $h2 * $r2 + $h3 * $r1 + $h4 * $r0;
155+
156+ $c = $d0 >> 26; $h0 = $d0 & $m;
157+ $d1 += $c; $c = $d1 >> 26; $h1 = $d1 & $m;
158+ $d2 += $c; $c = $d2 >> 26; $h2 = $d2 & $m;
159+ $d3 += $c; $c = $d3 >> 26; $h3 = $d3 & $m;
160+ $d4 += $c; $c = $d4 >> 26; $h4 = $d4 & $m;
161+ $h0 += $c * 5; $c = $h0 >> 26; $h0 &= $m;
162+ $h1 += $c;
163+ }
164+
165+ // 完全に繰り上げる
166+ $c = $h1 >> 26; $h1 &= $m;
167+ $h2 += $c; $c = $h2 >> 26; $h2 &= $m;
168+ $h3 += $c; $c = $h3 >> 26; $h3 &= $m;
169+ $h4 += $c; $c = $h4 >> 26; $h4 &= $m;
170+ $h0 += $c * 5; $c = $h0 >> 26; $h0 &= $m;
171+ $h1 += $c;
172+
173+ // h - p(h >= pならこちら)
174+ $g0 = $h0 + 5; $c = $g0 >> 26; $g0 &= $m;
175+ $g1 = $h1 + $c; $c = $g1 >> 26; $g1 &= $m;
176+ $g2 = $h2 + $c; $c = $g2 >> 26; $g2 &= $m;
177+ $g3 = $h3 + $c; $c = $g3 >> 26; $g3 &= $m;
178+ $g4 = $h4 + $c - (1 << 26);
179+ if ($g4 >= 0) {
180+ [$h0, $h1, $h2, $h3, $h4] = [$g0, $g1, $g2, $g3, $g4];
181+ }
182+
183+ // h + s(mod 2^128)
184+ $w0 = ($h0 | ($h1 << 26)) & 0xFFFFFFFF;
185+ $w1 = (($h1 >> 6) | ($h2 << 20)) & 0xFFFFFFFF;
186+ $w2 = (($h2 >> 12) | ($h3 << 14)) & 0xFFFFFFFF;
187+ $w3 = (($h3 >> 18) | ($h4 << 8)) & 0xFFFFFFFF;
188+ [, $p0, $p1, $p2, $p3] = unpack('V4', $key, 16);
189+ $f = $w0 + $p0; $w0 = $f & 0xFFFFFFFF;
190+ $f = $w1 + $p1 + ($f >> 32); $w1 = $f & 0xFFFFFFFF;
191+ $f = $w2 + $p2 + ($f >> 32); $w2 = $f & 0xFFFFFFFF;
192+ $f = $w3 + $p3 + ($f >> 32); $w3 = $f & 0xFFFFFFFF;
193+ return pack('V4', $w0, $w1, $w2, $w3);
194+ }
195+
196+ /** ChaCha20のブロック関数(RFC 8439 2.3) */
197+ private function block(int $counter, int $n0, int $n1, int $n2): string {
198+ [$k0, $k1, $k2, $k3, $k4, $k5, $k6, $k7] = $this->k;
199+ $x0 = 0x61707865; $x1 = 0x3320646e; $x2 = 0x79622d32; $x3 = 0x6b206574;
200+ $x4 = $k0; $x5 = $k1; $x6 = $k2; $x7 = $k3; $x8 = $k4; $x9 = $k5; $x10 = $k6; $x11 = $k7;
201+ $x12 = $counter; $x13 = $n0; $x14 = $n1; $x15 = $n2;
202+ $m = 0xFFFFFFFF;
203+
204+ for ($i = 0; $i < 10; $i++) {
205+ // 列
206+ $x0 = ($x0 + $x4) & $m; $x12 ^= $x0; $x12 = (($x12 << 16) | ($x12 >> 16)) & $m;
207+ $x8 = ($x8 + $x12) & $m; $x4 ^= $x8; $x4 = (($x4 << 12) | ($x4 >> 20)) & $m;
208+ $x0 = ($x0 + $x4) & $m; $x12 ^= $x0; $x12 = (($x12 << 8) | ($x12 >> 24)) & $m;
209+ $x8 = ($x8 + $x12) & $m; $x4 ^= $x8; $x4 = (($x4 << 7) | ($x4 >> 25)) & $m;
210+ $x1 = ($x1 + $x5) & $m; $x13 ^= $x1; $x13 = (($x13 << 16) | ($x13 >> 16)) & $m;
211+ $x9 = ($x9 + $x13) & $m; $x5 ^= $x9; $x5 = (($x5 << 12) | ($x5 >> 20)) & $m;
212+ $x1 = ($x1 + $x5) & $m; $x13 ^= $x1; $x13 = (($x13 << 8) | ($x13 >> 24)) & $m;
213+ $x9 = ($x9 + $x13) & $m; $x5 ^= $x9; $x5 = (($x5 << 7) | ($x5 >> 25)) & $m;
214+ $x2 = ($x2 + $x6) & $m; $x14 ^= $x2; $x14 = (($x14 << 16) | ($x14 >> 16)) & $m;
215+ $x10 = ($x10 + $x14) & $m; $x6 ^= $x10; $x6 = (($x6 << 12) | ($x6 >> 20)) & $m;
216+ $x2 = ($x2 + $x6) & $m; $x14 ^= $x2; $x14 = (($x14 << 8) | ($x14 >> 24)) & $m;
217+ $x10 = ($x10 + $x14) & $m; $x6 ^= $x10; $x6 = (($x6 << 7) | ($x6 >> 25)) & $m;
218+ $x3 = ($x3 + $x7) & $m; $x15 ^= $x3; $x15 = (($x15 << 16) | ($x15 >> 16)) & $m;
219+ $x11 = ($x11 + $x15) & $m; $x7 ^= $x11; $x7 = (($x7 << 12) | ($x7 >> 20)) & $m;
220+ $x3 = ($x3 + $x7) & $m; $x15 ^= $x3; $x15 = (($x15 << 8) | ($x15 >> 24)) & $m;
221+ $x11 = ($x11 + $x15) & $m; $x7 ^= $x11; $x7 = (($x7 << 7) | ($x7 >> 25)) & $m;
222+ // 対角線
223+ $x0 = ($x0 + $x5) & $m; $x15 ^= $x0; $x15 = (($x15 << 16) | ($x15 >> 16)) & $m;
224+ $x10 = ($x10 + $x15) & $m; $x5 ^= $x10; $x5 = (($x5 << 12) | ($x5 >> 20)) & $m;
225+ $x0 = ($x0 + $x5) & $m; $x15 ^= $x0; $x15 = (($x15 << 8) | ($x15 >> 24)) & $m;
226+ $x10 = ($x10 + $x15) & $m; $x5 ^= $x10; $x5 = (($x5 << 7) | ($x5 >> 25)) & $m;
227+ $x1 = ($x1 + $x6) & $m; $x12 ^= $x1; $x12 = (($x12 << 16) | ($x12 >> 16)) & $m;
228+ $x11 = ($x11 + $x12) & $m; $x6 ^= $x11; $x6 = (($x6 << 12) | ($x6 >> 20)) & $m;
229+ $x1 = ($x1 + $x6) & $m; $x12 ^= $x1; $x12 = (($x12 << 8) | ($x12 >> 24)) & $m;
230+ $x11 = ($x11 + $x12) & $m; $x6 ^= $x11; $x6 = (($x6 << 7) | ($x6 >> 25)) & $m;
231+ $x2 = ($x2 + $x7) & $m; $x13 ^= $x2; $x13 = (($x13 << 16) | ($x13 >> 16)) & $m;
232+ $x8 = ($x8 + $x13) & $m; $x7 ^= $x8; $x7 = (($x7 << 12) | ($x7 >> 20)) & $m;
233+ $x2 = ($x2 + $x7) & $m; $x13 ^= $x2; $x13 = (($x13 << 8) | ($x13 >> 24)) & $m;
234+ $x8 = ($x8 + $x13) & $m; $x7 ^= $x8; $x7 = (($x7 << 7) | ($x7 >> 25)) & $m;
235+ $x3 = ($x3 + $x4) & $m; $x14 ^= $x3; $x14 = (($x14 << 16) | ($x14 >> 16)) & $m;
236+ $x9 = ($x9 + $x14) & $m; $x4 ^= $x9; $x4 = (($x4 << 12) | ($x4 >> 20)) & $m;
237+ $x3 = ($x3 + $x4) & $m; $x14 ^= $x3; $x14 = (($x14 << 8) | ($x14 >> 24)) & $m;
238+ $x9 = ($x9 + $x14) & $m; $x4 ^= $x9; $x4 = (($x4 << 7) | ($x4 >> 25)) & $m;
239+ }
240+
241+ return pack('V16',
242+ ($x0 + 0x61707865) & $m, ($x1 + 0x3320646e) & $m, ($x2 + 0x79622d32) & $m, ($x3 + 0x6b206574) & $m,
243+ ($x4 + $k0) & $m, ($x5 + $k1) & $m, ($x6 + $k2) & $m, ($x7 + $k3) & $m,
244+ ($x8 + $k4) & $m, ($x9 + $k5) & $m, ($x10 + $k6) & $m, ($x11 + $k7) & $m,
245+ ($x12 + $counter) & $m, ($x13 + $n0) & $m, ($x14 + $n1) & $m, ($x15 + $n2) & $m);
246+ }
247+}
@@ -0,0 +1,443 @@
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\Crypto;
39+
40+/**
41+ * 楕円曲線(NISTのP-256・P-384・P-521):ECDH、ECDSA(RFC 6979の決定的な署名)、PEM
42+ *
43+ * 例:
44+ * $key = Ec::generate('P-256');
45+ * $shared = $key->ecdh($peerPoint); // 相手の公開鍵(04||x||y)
46+ * $sig = $key->sign($data, 'sha256'); // DER
47+ * $ok = Ec::fromPem($pub)->verify($data, $sig, 'sha256');
48+ */
49+class Ec {
50+ public const OID = '1.2.840.10045.2.1';
51+
52+ /** 曲線:p、a(=-3)、b、n、Gx、Gy、OID、バイト数 */
53+ private const CURVES = [
54+ 'P-256' => [
55+ 'p' => 'ffffffff00000001000000000000000000000000ffffffffffffffffffffffff',
56+ 'b' => '5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b',
57+ 'n' => 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551',
58+ 'x' => '6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296',
59+ 'y' => '4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5',
60+ 'oid' => '1.2.840.10045.3.1.7',
61+ 'len' => 32,
62+ ],
63+ 'P-384' => [
64+ 'p' => 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff',
65+ 'b' => 'b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef',
66+ 'n' => 'ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973',
67+ 'x' => 'aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7',
68+ 'y' => '3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f',
69+ 'oid' => '1.3.132.0.34',
70+ 'len' => 48,
71+ ],
72+ 'P-521' => [
73+ 'p' => '01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
74+ 'b' => '0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00',
75+ 'n' => '01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409',
76+ 'x' => '00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66',
77+ 'y' => '011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650',
78+ 'oid' => '1.3.132.0.35',
79+ 'len' => 66,
80+ ],
81+ ];
82+
83+ private static array $cache = [];
84+
85+ public readonly string $curve;
86+ /** 公開鍵の点 [x, y] */
87+ public readonly array $q;
88+ private ?\GMP $d = null;
89+ private array $c;
90+
91+ private function __construct(string $curve, array $q, ?\GMP $d = null) {
92+ $this->curve = $curve;
93+ $this->c = self::params($curve);
94+ $this->q = $q;
95+ $this->d = $d;
96+ }
97+
98+ /** 曲線のパラメータ(GMP) */
99+ private static function params(string $curve): array {
100+ if (isset(self::$cache[$curve])) return self::$cache[$curve];
101+ $c = self::CURVES[$curve] ?? throw new \InvalidArgumentException("EC:未対応の曲線:{$curve}");
102+ $p = gmp_init($c['p'], 16);
103+ return self::$cache[$curve] = [
104+ 'p' => $p,
105+ 'a' => gmp_sub($p, 3),
106+ 'b' => gmp_init($c['b'], 16),
107+ 'n' => gmp_init($c['n'], 16),
108+ 'g' => [gmp_init($c['x'], 16), gmp_init($c['y'], 16)],
109+ 'oid' => $c['oid'],
110+ 'len' => $c['len'],
111+ ];
112+ }
113+
114+ /** 対応している曲線 */
115+ public static function curves(): array {
116+ return array_keys(self::CURVES);
117+ }
118+
119+ /**
120+ * 新しい鍵を作る
121+ *
122+ * @param string $curve P-256、P-384、P-521
123+ * @return self
124+ */
125+ public static function generate(string $curve = 'P-256'): self {
126+ $c = self::params($curve);
127+ do {
128+ $d = gmp_import(random_bytes($c['len']));
129+ } while (gmp_sign($d) === 0 || gmp_cmp($d, $c['n']) >= 0);
130+ return new self($curve, self::mul($c, $d, $c['g']), $d);
131+ }
132+
133+ /**
134+ * 公開鍵の点から作る
135+ *
136+ * @param string $curve 曲線
137+ * @param string $point 圧縮しない点(04||x||y)
138+ * @return self
139+ * @throws \InvalidArgumentException 曲線上の点ではない場合
140+ */
141+ public static function fromPublic(string $curve, string $point): self {
142+ $c = self::params($curve);
143+ $l = $c['len'];
144+ if (strlen($point) !== 1 + 2 * $l || $point[0] !== "\x04") throw new \InvalidArgumentException('EC:未対応の点の形式');
145+ $q = [gmp_import(substr($point, 1, $l)), gmp_import(substr($point, 1 + $l, $l))];
146+ if (!self::onCurve($c, $q)) throw new \InvalidArgumentException('EC:曲線上の点ではない');
147+ return new self($curve, $q);
148+ }
149+
150+ /**
151+ * PEMから読む(PRIVATE KEY、EC PRIVATE KEY、PUBLIC KEY)
152+ *
153+ * @throws \InvalidArgumentException
154+ */
155+ public static function fromPem(string $pem): self {
156+ foreach (Asn1::unpem($pem) as $b) {
157+ if (in_array($b['label'], ['PRIVATE KEY', 'EC PRIVATE KEY', 'PUBLIC KEY'], true) && empty($b['encrypted'])) {
158+ return self::fromDer($b['der']);
159+ }
160+ }
161+ throw new \InvalidArgumentException('EC:鍵が見つからない');
162+ }
163+
164+ /**
165+ * DERから読む(PKCS#8、SEC 1、SubjectPublicKeyInfo)
166+ *
167+ * @throws \InvalidArgumentException
168+ */
169+ public static function fromDer(string $der, ?string $curve = null): self {
170+ try {
171+ $c = Asn1::expect(Asn1::parse($der), Asn1::SEQUENCE, 2);
172+
173+ // SubjectPublicKeyInfo
174+ if ($c[0]['tag'] === Asn1::SEQUENCE && $c[1]['tag'] === Asn1::BIT_STRING) {
175+ return self::fromPublic(self::curveOf($c[0]), Asn1::toBits($c[1]));
176+ }
177+
178+ // PKCS#8
179+ if ($c[0]['tag'] === Asn1::INTEGER && $c[1]['tag'] === Asn1::SEQUENCE && ($c[2]['tag'] ?? 0) === Asn1::OCTET_STRING) {
180+ return self::fromDer($c[2]['value'], self::curveOf($c[1]));
181+ }
182+
183+ // SEC 1:SEQUENCE { 1, OCTET STRING d, [0] OID, [1] BIT STRING }
184+ if ($c[0]['tag'] === Asn1::INTEGER && $c[1]['tag'] === Asn1::OCTET_STRING) {
185+ foreach (array_slice($c, 2) as $x) {
186+ if ($x['tag'] === 0xA0) $curve = self::curveName(Asn1::toOid(Asn1::children($x)[0]));
187+ }
188+ if ($curve === null) throw new \InvalidArgumentException('EC:曲線が分からない');
189+ $p = self::params($curve);
190+ $d = gmp_import($c[1]['value']);
191+ if (gmp_sign($d) === 0 || gmp_cmp($d, $p['n']) >= 0) throw new \InvalidArgumentException('EC:不正な秘密鍵');
192+ return new self($curve, self::mul($p, $d, $p['g']), $d);
193+ }
194+ } catch (\UnexpectedValueException $ex) {
195+ throw new \InvalidArgumentException('EC:壊れた鍵('.$ex->getMessage().')');
196+ }
197+ throw new \InvalidArgumentException('EC:未対応の鍵の形式');
198+ }
199+
200+ /** AlgorithmIdentifier { ecPublicKey, 曲線のOID } から曲線名 */
201+ private static function curveOf(array $alg): string {
202+ $a = Asn1::children($alg);
203+ if (Asn1::toOid($a[0]) !== self::OID || !isset($a[1]) || $a[1]['tag'] !== Asn1::OID) {
204+ throw new \InvalidArgumentException('EC:ECの鍵ではない');
205+ }
206+ return self::curveName(Asn1::toOid($a[1]));
207+ }
208+
209+ /** OIDから曲線名 */
210+ public static function curveName(string $oid): string {
211+ foreach (self::CURVES as $name => $c) if ($c['oid'] === $oid) return $name;
212+ throw new \InvalidArgumentException("EC:未対応の曲線:{$oid}");
213+ }
214+
215+ public function isPrivate(): bool {
216+ return $this->d !== null;
217+ }
218+
219+ /** 公開鍵の点(04||x||y) */
220+ public function publicPoint(): string {
221+ $l = $this->c['len'];
222+ return "\x04".self::pad($this->q[0], $l).self::pad($this->q[1], $l);
223+ }
224+
225+ /** 公開鍵(SubjectPublicKeyInfoのDER) */
226+ public function publicDer(): string {
227+ return Asn1::seq(Asn1::seq(Asn1::oid(self::OID), Asn1::oid($this->c['oid'])), Asn1::bits($this->publicPoint()));
228+ }
229+
230+ public function publicPem(): string {
231+ return Asn1::pem($this->publicDer(), 'PUBLIC KEY');
232+ }
233+
234+ /** 秘密鍵(PKCS#8のPEM) */
235+ public function toPem(): string {
236+ if ($this->d === null) throw new \LogicException('EC:秘密鍵ではない');
237+ $sec1 = Asn1::seq(Asn1::int(1), Asn1::octets(self::pad($this->d, $this->c['len'])), Asn1::explicit(1, Asn1::bits($this->publicPoint())));
238+ return Asn1::pem(Asn1::seq(Asn1::int(0), Asn1::seq(Asn1::oid(self::OID), Asn1::oid($this->c['oid'])), Asn1::octets($sec1)), 'PRIVATE KEY');
239+ }
240+
241+ /**
242+ * 共有の秘密を計算する(ECDH)
243+ *
244+ * @param string $peerPoint 相手の公開鍵(04||x||y)
245+ * @return string 共有の秘密のx座標
246+ * @throws \InvalidArgumentException 不正な点の場合
247+ */
248+ public function ecdh(string $peerPoint): string {
249+ if ($this->d === null) throw new \LogicException('EC:秘密鍵ではない');
250+ $peer = self::fromPublic($this->curve, $peerPoint);
251+ $s = self::mul($this->c, $this->d, $peer->q);
252+ if ($s === null) throw new \InvalidArgumentException('EC:共有の秘密が無限遠点');
253+ return self::pad($s[0], $this->c['len']);
254+ }
255+
256+ /**
257+ * 署名する(ECDSA、RFC 6979)
258+ *
259+ * @param string $msg メッセージ
260+ * @param string $hash ハッシュ
261+ * @return string DER(SEQUENCE { r, s })
262+ */
263+ public function sign(string $msg, string $hash = 'sha256'): string {
264+ if ($this->d === null) throw new \LogicException('EC:秘密鍵ではない');
265+ [$r, $s] = $this->signRaw(hash($hash, $msg, true), $hash);
266+ return Asn1::seq(Asn1::int($r), Asn1::int($s));
267+ }
268+
269+ /** 署名のrとs */
270+ private function signRaw(string $h1, string $hash): array {
271+ $n = $this->c['n'];
272+ $l = $this->c['len'];
273+ $z = $this->bits2int($h1);
274+ $x = self::pad($this->d, $l);
275+ $h = self::pad(gmp_mod($z, $n), $l);
276+
277+ // RFC 6979 3.2
278+ $hl = strlen(hash($hash, '', true));
279+ $v = str_repeat("\x01", $hl);
280+ $k = str_repeat("\x00", $hl);
281+ $k = hash_hmac($hash, $v."\x00".$x.$h, $k, true);
282+ $v = hash_hmac($hash, $v, $k, true);
283+ $k = hash_hmac($hash, $v."\x01".$x.$h, $k, true);
284+ $v = hash_hmac($hash, $v, $k, true);
285+
286+ while (true) {
287+ $t = '';
288+ while (strlen($t) < $l) {
289+ $v = hash_hmac($hash, $v, $k, true);
290+ $t .= $v;
291+ }
292+ $kk = $this->bits2int($t);
293+ if (gmp_sign($kk) > 0 && gmp_cmp($kk, $n) < 0) {
294+ $p = self::mul($this->c, $kk, $this->c['g']);
295+ $r = gmp_mod($p[0], $n);
296+ if (gmp_sign($r) > 0) {
297+ $s = gmp_mod(gmp_mul(gmp_invert($kk, $n), gmp_add($z, gmp_mul($r, $this->d))), $n);
298+ if (gmp_sign($s) > 0) return [$r, $s];
299+ }
300+ }
301+ $k = hash_hmac($hash, $v."\x00", $k, true);
302+ $v = hash_hmac($hash, $v, $k, true);
303+ }
304+ }
305+
306+ /**
307+ * 署名を確認する(ECDSA)
308+ *
309+ * @param string $msg メッセージ
310+ * @param string $sig DER(SEQUENCE { r, s })
311+ * @param string $hash ハッシュ
312+ * @return bool
313+ */
314+ public function verify(string $msg, string $sig, string $hash = 'sha256'): bool {
315+ try {
316+ $rs = Asn1::expect(Asn1::parse($sig), Asn1::SEQUENCE, 2);
317+ if (count($rs) !== 2) return false;
318+ $r = Asn1::toInt($rs[0]);
319+ $s = Asn1::toInt($rs[1]);
320+ } catch (\UnexpectedValueException $e) {
321+ return false;
322+ }
323+ // DERの形が正しい事(BERの別の書き方を拒否する)
324+ if (Asn1::seq(Asn1::int($r), Asn1::int($s)) !== $sig) return false;
325+
326+ $n = $this->c['n'];
327+ if (gmp_sign($r) <= 0 || gmp_sign($s) <= 0 || gmp_cmp($r, $n) >= 0 || gmp_cmp($s, $n) >= 0) return false;
328+
329+ $z = $this->bits2int(hash($hash, $msg, true));
330+ $w = gmp_invert($s, $n);
331+ $u1 = gmp_mod(gmp_mul($z, $w), $n);
332+ $u2 = gmp_mod(gmp_mul($r, $w), $n);
333+ $p = self::mul2($this->c, $u1, $this->c['g'], $u2, $this->q);
334+ return $p !== null && gmp_cmp(gmp_mod($p[0], $n), $r) === 0;
335+ }
336+
337+ /** ハッシュを整数にする(曲線のビット数に切り詰める) */
338+ private function bits2int(string $h): \GMP {
339+ $nbits = strlen(gmp_strval($this->c['n'], 2));
340+ $z = gmp_import($h);
341+ $hbits = 8 * strlen($h);
342+ return $hbits > $nbits ? gmp_div_q($z, gmp_pow(2, $hbits - $nbits)) : $z;
343+ }
344+
345+ ///////////////
346+ // 点の計算(ヤコビアン座標、a = -3)
347+
348+ private static function onCurve(array $c, array $q): bool {
349+ [$x, $y] = $q;
350+ $p = $c['p'];
351+ if (gmp_sign($x) < 0 || gmp_sign($y) < 0 || gmp_cmp($x, $p) >= 0 || gmp_cmp($y, $p) >= 0) return false;
352+ $l = gmp_mod(gmp_mul($y, $y), $p);
353+ $r = gmp_mod(gmp_add(gmp_add(gmp_mul(gmp_mul($x, $x), $x), gmp_mul($c['a'], $x)), $c['b']), $p);
354+ return gmp_cmp($l, $r) === 0;
355+ }
356+
357+ /** k * P(アフィン座標で返す、無限遠点はnull) */
358+ private static function mul(array $c, \GMP $k, array $pt): ?array {
359+ $bits = gmp_strval($k, 2);
360+ $jp = [$pt[0], $pt[1], gmp_init(1)];
361+ $r = null;
362+ for ($i = 0; $i < strlen($bits); $i++) {
363+ $r = self::dbl($c, $r);
364+ if ($bits[$i] === '1') $r = self::add($c, $r, $jp);
365+ }
366+ return self::affine($c, $r);
367+ }
368+
369+ /** u1 * P + u2 * Q(シャミアのトリック) */
370+ private static function mul2(array $c, \GMP $u1, array $p1, \GMP $u2, array $p2): ?array {
371+ $a = [$p1[0], $p1[1], gmp_init(1)];
372+ $b = [$p2[0], $p2[1], gmp_init(1)];
373+ $ab = self::add($c, $a, $b);
374+ $s1 = gmp_strval($u1, 2);
375+ $s2 = gmp_strval($u2, 2);
376+ $len = max(strlen($s1), strlen($s2));
377+ $s1 = str_pad($s1, $len, '0', STR_PAD_LEFT);
378+ $s2 = str_pad($s2, $len, '0', STR_PAD_LEFT);
379+ $r = null;
380+ for ($i = 0; $i < $len; $i++) {
381+ $r = self::dbl($c, $r);
382+ $bit = ($s1[$i] === '1' ? 1 : 0) | ($s2[$i] === '1' ? 2 : 0);
383+ if ($bit === 1) $r = self::add($c, $r, $a);
384+ else if ($bit === 2) $r = self::add($c, $r, $b);
385+ else if ($bit === 3) $r = self::add($c, $r, $ab);
386+ }
387+ return self::affine($c, $r);
388+ }
389+
390+ private static function dbl(array $c, ?array $P): ?array {
391+ if ($P === null) return null;
392+ [$x, $y, $z] = $P;
393+ if (gmp_sign($y) === 0) return null;
394+ $p = $c['p'];
395+ // a = -3:M = 3(X - Z²)(X + Z²)
396+ $zz = gmp_mod(gmp_mul($z, $z), $p);
397+ $m = gmp_mod(gmp_mul(3, gmp_mul(gmp_sub($x, $zz), gmp_add($x, $zz))), $p);
398+ $yy = gmp_mod(gmp_mul($y, $y), $p);
399+ $s = gmp_mod(gmp_mul(4, gmp_mul($x, $yy)), $p);
400+ $x3 = gmp_mod(gmp_sub(gmp_mul($m, $m), gmp_mul(2, $s)), $p);
401+ $y3 = gmp_mod(gmp_sub(gmp_mul($m, gmp_sub($s, $x3)), gmp_mul(8, gmp_mul($yy, $yy))), $p);
402+ $z3 = gmp_mod(gmp_mul(2, gmp_mul($y, $z)), $p);
403+ return [$x3, $y3, $z3];
404+ }
405+
406+ private static function add(array $c, ?array $P, ?array $Q): ?array {
407+ if ($P === null) return $Q;
408+ if ($Q === null) return $P;
409+ $p = $c['p'];
410+ [$x1, $y1, $z1] = $P;
411+ [$x2, $y2, $z2] = $Q;
412+ $z1z1 = gmp_mod(gmp_mul($z1, $z1), $p);
413+ $z2z2 = gmp_mod(gmp_mul($z2, $z2), $p);
414+ $u1 = gmp_mod(gmp_mul($x1, $z2z2), $p);
415+ $u2 = gmp_mod(gmp_mul($x2, $z1z1), $p);
416+ $s1 = gmp_mod(gmp_mul($y1, gmp_mul($z2, $z2z2)), $p);
417+ $s2 = gmp_mod(gmp_mul($y2, gmp_mul($z1, $z1z1)), $p);
418+ $h = gmp_mod(gmp_sub($u2, $u1), $p);
419+ $r = gmp_mod(gmp_sub($s2, $s1), $p);
420+ if (gmp_sign($h) === 0) {
421+ return gmp_sign($r) === 0 ? self::dbl($c, $P) : null;
422+ }
423+ $hh = gmp_mod(gmp_mul($h, $h), $p);
424+ $hhh = gmp_mod(gmp_mul($h, $hh), $p);
425+ $v = gmp_mod(gmp_mul($u1, $hh), $p);
426+ $x3 = gmp_mod(gmp_sub(gmp_sub(gmp_mul($r, $r), $hhh), gmp_mul(2, $v)), $p);
427+ $y3 = gmp_mod(gmp_sub(gmp_mul($r, gmp_sub($v, $x3)), gmp_mul($s1, $hhh)), $p);
428+ $z3 = gmp_mod(gmp_mul($h, gmp_mul($z1, $z2)), $p);
429+ return [$x3, $y3, $z3];
430+ }
431+
432+ private static function affine(array $c, ?array $P): ?array {
433+ if ($P === null || gmp_sign($P[2]) === 0) return null;
434+ $p = $c['p'];
435+ $zi = gmp_invert($P[2], $p);
436+ $zi2 = gmp_mod(gmp_mul($zi, $zi), $p);
437+ return [gmp_mod(gmp_mul($P[0], $zi2), $p), gmp_mod(gmp_mul($P[1], gmp_mul($zi2, $zi)), $p)];
438+ }
439+
440+ private static function pad(\GMP $n, int $len): string {
441+ return str_pad(gmp_export($n), $len, "\0", STR_PAD_LEFT);
442+ }
443+}
@@ -0,0 +1,439 @@
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\Crypto;
39+
40+/**
41+ * RSA(鍵の作成、PEM、PKCS#1 v1.5・PSSの署名、OAEPの暗号化)
42+ *
43+ * 例:
44+ * $key = Rsa::generate(2048);
45+ * file_put_contents('priv.pem', $key->toPem());
46+ * $sig = Rsa::fromPem($priv)->sign($data); // RSASSA-PKCS1-v1_5、SHA-256
47+ * $ok = Rsa::fromPem($pub)->verify($data, $sig);
48+ * $ct = Rsa::fromPem($pub)->encrypt($secret, 'sha1'); // RSAES-OAEP
49+ *
50+ * 秘密鍵の計算はCRTとブラインディングを使い、結果を公開鍵で確認する。
51+ */
52+class Rsa {
53+ public const OID = '1.2.840.113549.1.1.1';
54+ public const OID_PSS = '1.2.840.113549.1.1.10';
55+
56+ // DigestInfoの先頭(RFC 8017 9.2)
57+ private const DIGEST_INFO = [
58+ 'sha1' => "\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14",
59+ 'sha224' => "\x30\x2d\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x04\x05\x00\x04\x1c",
60+ 'sha256' => "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20",
61+ 'sha384' => "\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30",
62+ 'sha512' => "\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40",
63+ ];
64+
65+ public readonly \GMP $n;
66+ public readonly \GMP $e;
67+ /** 法の大きさ(ビット、バイト) */
68+ public readonly int $bits;
69+ public readonly int $bytes;
70+
71+ private ?\GMP $d = null;
72+ private ?\GMP $p = null;
73+ private ?\GMP $q = null;
74+ private ?\GMP $dp = null;
75+ private ?\GMP $dq = null;
76+ private ?\GMP $qi = null;
77+
78+ private function __construct(\GMP $n, \GMP $e) {
79+ if (gmp_cmp($n, 0) <= 0 || gmp_cmp($e, 1) <= 0) throw new \InvalidArgumentException('RSA:不正な鍵');
80+ $this->n = $n;
81+ $this->e = $e;
82+ $this->bits = strlen(gmp_strval($n, 2));
83+ $this->bytes = intdiv($this->bits + 7, 8);
84+ }
85+
86+ /**
87+ * 新しい鍵を作る
88+ *
89+ * @param int $bits 法の大きさ(1024以上、普通は2048か4096)
90+ * @param int $e 公開指数
91+ * @return self
92+ */
93+ public static function generate(int $bits = 2048, int $e = 65537): self {
94+ if ($bits < 1024 || $bits % 2) throw new \InvalidArgumentException('RSA:鍵の大きさは1024以上の偶数');
95+ $E = gmp_init($e);
96+
97+ while (true) {
98+ $p = self::prime(intdiv($bits, 2), $E);
99+ $q = self::prime(intdiv($bits, 2), $E);
100+ // pとqが近過ぎない事(FIPS 186-4 B.3.1)
101+ if (strlen(gmp_strval(gmp_abs(gmp_sub($p, $q)), 2)) <= intdiv($bits, 2) - 100) continue;
102+ $n = gmp_mul($p, $q);
103+ if (strlen(gmp_strval($n, 2)) !== $bits) continue;
104+ return self::fromParts($n, $E, null, $p, $q);
105+ }
106+ }
107+
108+ /** 最上位の2ビットを立てた、p-1がeと互いに素な素数 */
109+ private static function prime(int $bits, \GMP $e): \GMP {
110+ while (true) {
111+ $b = random_bytes(intdiv($bits + 7, 8));
112+ $b[0] = chr(ord($b[0]) | 0xC0);
113+ $c = gmp_nextprime(gmp_import($b));
114+ if (strlen(gmp_strval($c, 2)) !== $bits) continue;
115+ if (gmp_cmp(gmp_gcd(gmp_sub($c, 1), $e), 1) !== 0) continue;
116+ if (gmp_prob_prime($c, 40) === 0) continue;
117+ return $c;
118+ }
119+ }
120+
121+ /** n、e、(d、p、q)から作る */
122+ private static function fromParts(\GMP $n, \GMP $e, ?\GMP $d = null, ?\GMP $p = null, ?\GMP $q = null): self {
123+ $key = new self($n, $e);
124+ if ($p !== null && $q !== null) {
125+ if (gmp_cmp(gmp_mul($p, $q), $n) !== 0) throw new \InvalidArgumentException('RSA:p・qとnが合わない');
126+ $phi = gmp_lcm(gmp_sub($p, 1), gmp_sub($q, 1));
127+ $d ??= gmp_invert($e, $phi) ?: null;
128+ if ($d === null) throw new \InvalidArgumentException('RSA:dを計算出来ない');
129+ $key->p = $p;
130+ $key->q = $q;
131+ $key->dp = gmp_mod($d, gmp_sub($p, 1));
132+ $key->dq = gmp_mod($d, gmp_sub($q, 1));
133+ $key->qi = gmp_invert($q, $p) ?: null;
134+ }
135+ $key->d = $d;
136+ return $key;
137+ }
138+
139+ ///////////////
140+ // 読み込み・書き出し
141+
142+ /**
143+ * PEMから読む(PRIVATE KEY、RSA PRIVATE KEY、PUBLIC KEY、RSA PUBLIC KEY)
144+ *
145+ * @param string $pem
146+ * @return self
147+ * @throws \InvalidArgumentException RSAの鍵ではない、又は暗号化されている場合
148+ */
149+ public static function fromPem(string $pem): self {
150+ foreach (Asn1::unpem($pem) as $b) {
151+ if (!empty($b['encrypted']) || $b['label'] === 'ENCRYPTED PRIVATE KEY') {
152+ throw new \InvalidArgumentException('RSA:暗号化された鍵は未対応');
153+ }
154+ switch ($b['label']) {
155+ case 'PRIVATE KEY':
156+ case 'PUBLIC KEY':
157+ case 'RSA PRIVATE KEY':
158+ case 'RSA PUBLIC KEY':
159+ return self::fromDer($b['der']);
160+ }
161+ }
162+ throw new \InvalidArgumentException('RSA:鍵が見つからない');
163+ }
164+
165+ /**
166+ * DERから読む(PKCS#8、SubjectPublicKeyInfo、PKCS#1の秘密鍵・公開鍵)
167+ *
168+ * @param string $der
169+ * @return self
170+ * @throws \InvalidArgumentException
171+ */
172+ public static function fromDer(string $der): self {
173+ try {
174+ $c = Asn1::expect(Asn1::parse($der), Asn1::SEQUENCE, 2);
175+
176+ // SubjectPublicKeyInfo:SEQUENCE { AlgorithmIdentifier, BIT STRING }
177+ if ($c[0]['tag'] === Asn1::SEQUENCE && $c[1]['tag'] === Asn1::BIT_STRING) {
178+ self::checkAlgorithm($c[0]);
179+ return self::fromDer(Asn1::toBits($c[1]));
180+ }
181+
182+ // PKCS#8:SEQUENCE { version, AlgorithmIdentifier, OCTET STRING, … }
183+ if ($c[0]['tag'] === Asn1::INTEGER && $c[1]['tag'] === Asn1::SEQUENCE && ($c[2]['tag'] ?? 0) === Asn1::OCTET_STRING) {
184+ self::checkAlgorithm($c[1]);
185+ return self::fromDer($c[2]['value']);
186+ }
187+
188+ // PKCS#1の公開鍵:SEQUENCE { n, e }
189+ if (count($c) === 2) return new self(Asn1::toInt($c[0]), Asn1::toInt($c[1]));
190+
191+ // PKCS#1の秘密鍵:SEQUENCE { version, n, e, d, p, q, dp, dq, qi }
192+ if (count($c) >= 9 && gmp_sign(Asn1::toInt($c[0])) === 0) {
193+ [$n, $e, $d, $p, $q] = array_map([Asn1::class, 'toInt'], array_slice($c, 1, 5));
194+ return self::fromParts($n, $e, $d, $p, $q);
195+ }
196+ } catch (\UnexpectedValueException $ex) {
197+ throw new \InvalidArgumentException('RSA:壊れた鍵('.$ex->getMessage().')');
198+ }
199+
200+ throw new \InvalidArgumentException('RSA:未対応の鍵の形式');
201+ }
202+
203+ /**
204+ * SubjectPublicKeyInfoのアルゴリズムを確認する
205+ *
206+ * @throws \InvalidArgumentException RSA(又はRSA-PSS)ではない場合
207+ */
208+ private static function checkAlgorithm(array $alg): void {
209+ $oid = Asn1::toOid(Asn1::children($alg)[0]);
210+ if ($oid !== self::OID && $oid !== self::OID_PSS) throw new \InvalidArgumentException("RSA:RSAの鍵ではない({$oid})");
211+ }
212+
213+ public function isPrivate(): bool {
214+ return $this->d !== null;
215+ }
216+
217+ /** 公開鍵(SubjectPublicKeyInfoのDER) */
218+ public function publicDer(): string {
219+ return Asn1::seq(
220+ Asn1::seq(Asn1::oid(self::OID), Asn1::null()),
221+ Asn1::bits(Asn1::seq(Asn1::int($this->n), Asn1::int($this->e))),
222+ );
223+ }
224+
225+ /** 公開鍵(「BEGIN PUBLIC KEY」のPEM) */
226+ public function publicPem(): string {
227+ return Asn1::pem($this->publicDer(), 'PUBLIC KEY');
228+ }
229+
230+ /**
231+ * 秘密鍵(PKCS#8、「BEGIN PRIVATE KEY」のPEM)
232+ *
233+ * @throws \LogicException 公開鍵の場合
234+ */
235+ public function toPem(): string {
236+ if ($this->p === null) throw new \LogicException('RSA:秘密鍵ではない');
237+ $pkcs1 = Asn1::seq(Asn1::int(0), Asn1::int($this->n), Asn1::int($this->e), Asn1::int($this->d),
238+ Asn1::int($this->p), Asn1::int($this->q), Asn1::int($this->dp), Asn1::int($this->dq), Asn1::int($this->qi));
239+ return Asn1::pem(Asn1::seq(Asn1::int(0), Asn1::seq(Asn1::oid(self::OID), Asn1::null()), Asn1::octets($pkcs1)), 'PRIVATE KEY');
240+ }
241+
242+ ///////////////
243+ // 署名
244+
245+ /**
246+ * 署名する(RSASSA-PKCS1-v1_5)
247+ *
248+ * @param string $msg メッセージ
249+ * @param string $hash sha1、sha256、sha384、sha512等
250+ * @return string 署名($this->bytesバイト)
251+ */
252+ public function sign(string $msg, string $hash = 'sha256'): string {
253+ return $this->private($this->pkcs1Encode($msg, $hash));
254+ }
255+
256+ /**
257+ * 署名を確認する(RSASSA-PKCS1-v1_5)
258+ *
259+ * @param string $msg メッセージ
260+ * @param string $sig 署名
261+ * @param string $hash ハッシュ
262+ * @return bool
263+ */
264+ public function verify(string $msg, string $sig, string $hash = 'sha256'): bool {
265+ $em = $this->public($sig);
266+ return $em !== null && hash_equals($this->pkcs1Encode($msg, $hash), $em);
267+ }
268+
269+ private function pkcs1Encode(string $msg, string $hash): string {
270+ if (!isset(self::DIGEST_INFO[$hash])) throw new \InvalidArgumentException("RSA:未対応のハッシュ:{$hash}");
271+ $t = self::DIGEST_INFO[$hash].hash($hash, $msg, true);
272+ if ($this->bytes < strlen($t) + 11) throw new \InvalidArgumentException('RSA:鍵が小さ過ぎる');
273+ return "\x00\x01".str_repeat("\xFF", $this->bytes - strlen($t) - 3)."\x00".$t;
274+ }
275+
276+ /**
277+ * 署名する(RSASSA-PSS、MGF1は同じハッシュ)
278+ *
279+ * @param string $msg メッセージ
280+ * @param string $hash ハッシュ
281+ * @param int|null $saltLen ソルトの長さ(null=ハッシュの長さ)
282+ * @return string
283+ */
284+ public function signPss(string $msg, string $hash = 'sha256', ?int $saltLen = null): string {
285+ $mHash = hash($hash, $msg, true);
286+ $hLen = strlen($mHash);
287+ $saltLen ??= $hLen;
288+ $emBits = $this->bits - 1;
289+ $emLen = intdiv($emBits + 7, 8);
290+ if ($emLen < $hLen + $saltLen + 2) throw new \InvalidArgumentException('RSA:鍵が小さ過ぎる');
291+
292+ $salt = $saltLen > 0 ? random_bytes($saltLen) : '';
293+ $h = hash($hash, str_repeat("\0", 8).$mHash.$salt, true);
294+ $db = str_repeat("\0", $emLen - $saltLen - $hLen - 2)."\x01".$salt;
295+ $masked = $db ^ self::mgf1($h, $emLen - $hLen - 1, $hash);
296+ $masked[0] = chr(ord($masked[0]) & (0xFF >> (8 * $emLen - $emBits)));
297+ return $this->private(str_pad($masked.$h."\xBC", $this->bytes, "\0", STR_PAD_LEFT));
298+ }
299+
300+ /**
301+ * 署名を確認する(RSASSA-PSS)
302+ *
303+ * @param string $msg メッセージ
304+ * @param string $sig 署名
305+ * @param string $hash ハッシュ(MGF1も同じ)
306+ * @param int|null $saltLen ソルトの長さ(null=ハッシュの長さ、-1=何でも)
307+ * @return bool
308+ */
309+ public function verifyPss(string $msg, string $sig, string $hash = 'sha256', ?int $saltLen = null): bool {
310+ $em = $this->public($sig);
311+ if ($em === null) return false;
312+
313+ $mHash = hash($hash, $msg, true);
314+ $hLen = strlen($mHash);
315+ $emBits = $this->bits - 1;
316+ $emLen = intdiv($emBits + 7, 8);
317+ $em = substr($em, -$emLen);
318+ if (strlen($em) !== $emLen || $emLen < $hLen + 2 || substr($em, -1) !== "\xBC") return false;
319+ if ($this->bytes > $emLen && ltrim(substr($this->public($sig), 0, $this->bytes - $emLen), "\0") !== '') return false;
320+
321+ $masked = substr($em, 0, $emLen - $hLen - 1);
322+ $h = substr($em, $emLen - $hLen - 1, $hLen);
323+ $topMask = 0xFF >> (8 * $emLen - $emBits);
324+ if (ord($masked[0]) & ~$topMask & 0xFF) return false;
325+
326+ $db = $masked ^ self::mgf1($h, strlen($masked), $hash);
327+ $db[0] = chr(ord($db[0]) & $topMask);
328+ $one = strpos($db, "\x01");
329+ if ($one === false || ltrim(substr($db, 0, $one), "\0") !== '') return false;
330+ $salt = substr($db, $one + 1);
331+ if ($saltLen !== null && $saltLen >= 0 && strlen($salt) !== $saltLen) return false;
332+ if ($saltLen === null && strlen($salt) !== $hLen) return false;
333+
334+ return hash_equals($h, hash($hash, str_repeat("\0", 8).$mHash.$salt, true));
335+ }
336+
337+ ///////////////
338+ // 暗号化
339+
340+ /**
341+ * 暗号化する(RSAES-OAEP、MGF1は同じハッシュ)
342+ *
343+ * @param string $msg 平文
344+ * @param string $hash ハッシュ(OpenSSLのOPENSSL_PKCS1_OAEP_PADDINGはsha1)
345+ * @param string $label ラベル
346+ * @return string
347+ */
348+ public function encrypt(string $msg, string $hash = 'sha1', string $label = ''): string {
349+ $lHash = hash($hash, $label, true);
350+ $hLen = strlen($lHash);
351+ $k = $this->bytes;
352+ if (strlen($msg) > $k - 2 * $hLen - 2) throw new \InvalidArgumentException('RSA:メッセージが長過ぎる');
353+
354+ $db = $lHash.str_repeat("\0", $k - strlen($msg) - 2 * $hLen - 2)."\x01".$msg;
355+ $seed = random_bytes($hLen);
356+ $maskedDb = $db ^ self::mgf1($seed, $k - $hLen - 1, $hash);
357+ $maskedSeed = $seed ^ self::mgf1($maskedDb, $hLen, $hash);
358+ return $this->public("\x00".$maskedSeed.$maskedDb, false);
359+ }
360+
361+ /**
362+ * 復号する(RSAES-OAEP)
363+ *
364+ * @param string $ct 暗号文
365+ * @param string $hash ハッシュ
366+ * @param string $label ラベル
367+ * @return string|null 平文、失敗したらnull
368+ */
369+ public function decrypt(string $ct, string $hash = 'sha1', string $label = ''): ?string {
370+ if (strlen($ct) !== $this->bytes) return null;
371+ $em = $this->private($ct);
372+ $lHash = hash($hash, $label, true);
373+ $hLen = strlen($lHash);
374+ if ($this->bytes < 2 * $hLen + 2) return null;
375+
376+ $maskedSeed = substr($em, 1, $hLen);
377+ $maskedDb = substr($em, 1 + $hLen);
378+ $seed = $maskedSeed ^ self::mgf1($maskedDb, $hLen, $hash);
379+ $db = $maskedDb ^ self::mgf1($seed, strlen($maskedDb), $hash);
380+
381+ $one = strpos($db, "\x01", $hLen);
382+ $bad = ($em[0] !== "\0") | !hash_equals($lHash, substr($db, 0, $hLen)) | ($one === false);
383+ if ($bad || ltrim(substr($db, $hLen, $one - $hLen), "\0") !== '') return null;
384+ return substr($db, $one + 1);
385+ }
386+
387+ ///////////////
388+ // 計算
389+
390+ /** MGF1(RFC 8017 B.2.1) */
391+ private static function mgf1(string $seed, int $len, string $hash): string {
392+ $out = '';
393+ for ($i = 0; strlen($out) < $len; $i++) $out .= hash($hash, $seed.pack('N', $i), true);
394+ return substr($out, 0, $len);
395+ }
396+
397+ /** 公開鍵の計算(m^e mod n) */
398+ private function public(string $data, bool $check = true): ?string {
399+ if (strlen($data) !== $this->bytes) {
400+ if ($check) return null;
401+ throw new \InvalidArgumentException('RSA:長さが違う');
402+ }
403+ $m = gmp_import($data);
404+ if (gmp_cmp($m, $this->n) >= 0) return null;
405+ return $this->i2osp(gmp_powm($m, $this->e, $this->n));
406+ }
407+
408+ /** 秘密鍵の計算(CRT、ブラインディング、結果の確認) */
409+ private function private(string $data): string {
410+ if ($this->d === null) throw new \LogicException('RSA:秘密鍵ではない');
411+ $c = gmp_import($data);
412+ if (gmp_cmp($c, $this->n) >= 0) throw new \InvalidArgumentException('RSA:値が大き過ぎる');
413+
414+ // ブラインディング:c' = c * r^e
415+ do {
416+ $r = gmp_mod(gmp_import(random_bytes($this->bytes + 8)), $this->n);
417+ $ri = gmp_invert($r, $this->n);
418+ } while ($ri === false || gmp_sign($r) === 0);
419+ $cb = gmp_mod(gmp_mul($c, gmp_powm($r, $this->e, $this->n)), $this->n);
420+
421+ if ($this->p !== null && $this->qi !== null) {
422+ $m1 = gmp_powm($cb, $this->dp, $this->p);
423+ $m2 = gmp_powm($cb, $this->dq, $this->q);
424+ $h = gmp_mod(gmp_mul($this->qi, gmp_sub($m1, $m2)), $this->p);
425+ $mb = gmp_add($m2, gmp_mul($h, $this->q));
426+ } else {
427+ $mb = gmp_powm($cb, $this->d, $this->n);
428+ }
429+ $m = gmp_mod(gmp_mul($mb, $ri), $this->n);
430+
431+ // 計算の誤り(故障攻撃)を防ぐ
432+ if (gmp_cmp(gmp_powm($m, $this->e, $this->n), $c) !== 0) throw new \RuntimeException('RSA:計算に失敗');
433+ return $this->i2osp($m);
434+ }
435+
436+ private function i2osp(\GMP $x): string {
437+ return str_pad(gmp_export($x), $this->bytes, "\0", STR_PAD_LEFT);
438+ }
439+}
@@ -0,0 +1,126 @@
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\Crypto;
39+
40+/**
41+ * X25519(RFC 7748)の鍵交換
42+ *
43+ * 例:
44+ * [$priv, $pub] = X25519::generate();
45+ * $shared = X25519::sharedSecret($priv, $peerPub);
46+ */
47+class X25519 {
48+ private static ?\GMP $p = null;
49+
50+ /**
51+ * 新しい鍵の組
52+ *
53+ * @return array [秘密鍵(32バイト), 公開鍵(32バイト)]
54+ */
55+ public static function generate(): array {
56+ $priv = random_bytes(32);
57+ return [$priv, self::publicKey($priv)];
58+ }
59+
60+ /** 秘密鍵から公開鍵 */
61+ public static function publicKey(string $priv): string {
62+ return self::ladder($priv, "\x09".str_repeat("\0", 31));
63+ }
64+
65+ /**
66+ * 共有の秘密
67+ *
68+ * @param string $priv 自分の秘密鍵
69+ * @param string $pub 相手の公開鍵
70+ * @return string 32バイト
71+ * @throws \InvalidArgumentException 長さが違う、又は結果が全部0(小さい位数の点)の場合
72+ */
73+ public static function sharedSecret(string $priv, string $pub): string {
74+ $s = self::ladder($priv, $pub);
75+ if ($s === str_repeat("\0", 32)) throw new \InvalidArgumentException('X25519:不正な公開鍵');
76+ return $s;
77+ }
78+
79+ /** モンゴメリ・ラダー(RFC 7748 5) */
80+ private static function ladder(string $k, string $u): string {
81+ if (strlen($k) !== 32 || strlen($u) !== 32) throw new \InvalidArgumentException('X25519:鍵は32バイト');
82+ $p = self::$p ??= gmp_sub(gmp_pow(2, 255), 19);
83+
84+ $k[0] = chr(ord($k[0]) & 248);
85+ $k[31] = chr((ord($k[31]) & 127) | 64);
86+ $u[31] = chr(ord($u[31]) & 127);
87+ $scalar = gmp_import(strrev($k));
88+ $x1 = gmp_mod(gmp_import(strrev($u)), $p);
89+
90+ $x2 = gmp_init(1);
91+ $z2 = gmp_init(0);
92+ $x3 = $x1;
93+ $z3 = gmp_init(1);
94+ $swap = 0;
95+
96+ for ($t = 254; $t >= 0; $t--) {
97+ $kt = gmp_testbit($scalar, $t) ? 1 : 0;
98+ if ($swap ^ $kt) {
99+ [$x2, $x3] = [$x3, $x2];
100+ [$z2, $z3] = [$z3, $z2];
101+ }
102+ $swap = $kt;
103+
104+ $a = gmp_add($x2, $z2);
105+ $aa = gmp_mod(gmp_mul($a, $a), $p);
106+ $b = gmp_sub($x2, $z2);
107+ $bb = gmp_mod(gmp_mul($b, $b), $p);
108+ $e = gmp_sub($aa, $bb);
109+ $c = gmp_add($x3, $z3);
110+ $d = gmp_sub($x3, $z3);
111+ $da = gmp_mul($d, $a);
112+ $cb = gmp_mul($c, $b);
113+ $x3 = gmp_mod(gmp_pow(gmp_add($da, $cb), 2), $p);
114+ $z3 = gmp_mod(gmp_mul($x1, gmp_pow(gmp_sub($da, $cb), 2)), $p);
115+ $x2 = gmp_mod(gmp_mul($aa, $bb), $p);
116+ $z2 = gmp_mod(gmp_mul($e, gmp_add($aa, gmp_mul(121665, $e))), $p);
117+ }
118+ if ($swap) {
119+ [$x2, $x3] = [$x3, $x2];
120+ [$z2, $z3] = [$z3, $z2];
121+ }
122+
123+ $r = gmp_mod(gmp_mul($x2, gmp_powm($z2, gmp_sub($p, 2), $p)), $p);
124+ return strrev(str_pad(gmp_export($r), 32, "\0", STR_PAD_LEFT));
125+ }
126+}
@@ -0,0 +1,552 @@
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\Crypto;
39+
40+/**
41+ * X.509の証明書:読み込み、署名・有効期限・ホスト名の確認、信頼出来る証明書までの経路の確認、作成
42+ *
43+ * 例:
44+ * $chain = array_map(fn($der) => new X509($der), $ders);
45+ * X509::verifyChain($chain, 'example.com'); // 失敗すると\RuntimeException
46+ * $cert = X509::issue($key, 'localhost', ['sans' => ['localhost', '127.0.0.1']]); // 自己署名
47+ */
48+class X509 {
49+ // 署名のアルゴリズム => [鍵の種類, ハッシュ]
50+ private const SIG_ALGS = [
51+ '1.2.840.113549.1.1.5' => ['rsa', 'sha1'],
52+ '1.2.840.113549.1.1.14' => ['rsa', 'sha224'],
53+ '1.2.840.113549.1.1.11' => ['rsa', 'sha256'],
54+ '1.2.840.113549.1.1.12' => ['rsa', 'sha384'],
55+ '1.2.840.113549.1.1.13' => ['rsa', 'sha512'],
56+ '1.2.840.113549.1.1.10' => ['pss', null],
57+ '1.2.840.10045.4.1' => ['ec', 'sha1'],
58+ '1.2.840.10045.4.3.2' => ['ec', 'sha256'],
59+ '1.2.840.10045.4.3.3' => ['ec', 'sha384'],
60+ '1.2.840.10045.4.3.4' => ['ec', 'sha512'],
61+ ];
62+ private const HASH_OIDS = [
63+ '1.3.14.3.2.26' => 'sha1',
64+ '2.16.840.1.101.3.4.2.4' => 'sha224',
65+ '2.16.840.1.101.3.4.2.1' => 'sha256',
66+ '2.16.840.1.101.3.4.2.2' => 'sha384',
67+ '2.16.840.1.101.3.4.2.3' => 'sha512',
68+ ];
69+ private const NAMES = [
70+ '2.5.4.3' => 'CN', '2.5.4.6' => 'C', '2.5.4.7' => 'L', '2.5.4.8' => 'ST', '2.5.4.10' => 'O',
71+ '2.5.4.11' => 'OU', '2.5.4.5' => 'serialNumber', '1.2.840.113549.1.9.1' => 'emailAddress', '0.9.2342.19200300.100.1.25' => 'DC',
72+ ];
73+ // 理解している拡張(重要でも拒否しない)
74+ private const KNOWN_EXT = ['2.5.29.15', '2.5.29.17', '2.5.29.19', '2.5.29.30', '2.5.29.37', '2.5.29.32', '2.5.29.36', '2.5.29.54', '2.5.29.14', '2.5.29.35'];
75+ private const EKU_SERVER = '1.3.6.1.5.5.7.3.1';
76+ private const EKU_ANY = '2.5.29.37.0';
77+ private const MAX_DEPTH = 10;
78+
79+ /** 証明書の場所(TLS_CAFILEが無い場合、順に探す) */
80+ public const CA_FILES = [
81+ '/etc/ssl/cert.pem', // OpenBSD、macOS、Alpine(chrootの中なら/var/www/etc/ssl/cert.pem)
82+ '/etc/ssl/certs/ca-certificates.crt', // Debian、Ubuntu
83+ '/etc/pki/tls/certs/ca-bundle.crt', // RHEL、Fedora
84+ '/etc/ssl/ca-bundle.pem', // openSUSE
85+ '/usr/local/share/certs/ca-root-nss.crt', // FreeBSD
86+ ];
87+
88+ private static array $stores = [];
89+
90+ public readonly string $der;
91+ public readonly string $tbs;
92+ public readonly int $version;
93+ public readonly string $serial;
94+ public readonly string $issuerRaw;
95+ public readonly string $subjectRaw;
96+ /** 「CN=…, O=…」の形 */
97+ public readonly string $issuer;
98+ public readonly string $subject;
99+ public readonly int $notBefore;
100+ public readonly int $notAfter;
101+ public readonly string $spki;
102+ public readonly string $sigAlg;
103+ private array $sigParams;
104+ private string $signature;
105+ /** 拡張:OID => ['critical' => bool, 'value' => DER] */
106+ public readonly array $extensions;
107+ private ?array $subjectAttrs = null;
108+
109+ /**
110+ * @param string $der DER(PEMはfromPem())
111+ * @throws \UnexpectedValueException 壊れた証明書の場合
112+ */
113+ public function __construct(string $der) {
114+ $this->der = $der;
115+ $c = Asn1::expect(Asn1::parse($der), Asn1::SEQUENCE, 3);
116+ $this->tbs = $c[0]['raw'];
117+ $alg = Asn1::expect($c[1], Asn1::SEQUENCE, 1);
118+ $this->sigAlg = Asn1::toOid($alg[0]);
119+ $this->sigParams = array_slice($alg, 1);
120+ $this->signature = Asn1::toBits($c[2]);
121+
122+ $t = Asn1::expect($c[0], Asn1::SEQUENCE, 6);
123+ $i = 0;
124+ $version = 1;
125+ if ($t[0]['tag'] === 0xA0) {
126+ $version = gmp_intval(Asn1::toInt(Asn1::children($t[0])[0])) + 1;
127+ $i = 1;
128+ }
129+ $this->version = $version;
130+ $this->serial = gmp_strval(Asn1::toInt($t[$i]), 16);
131+ $this->issuerRaw = $t[$i + 2]['raw'];
132+ $validity = Asn1::expect($t[$i + 3], Asn1::SEQUENCE, 2);
133+ $this->notBefore = Asn1::toTime($validity[0]);
134+ $this->notAfter = Asn1::toTime($validity[1]);
135+ $this->subjectRaw = $t[$i + 4]['raw'];
136+ $this->spki = $t[$i + 5]['raw'];
137+ $this->issuer = self::nameString($t[$i + 2]);
138+ $this->subject = self::nameString($t[$i + 4]);
139+
140+ $ext = [];
141+ foreach (array_slice($t, $i + 6) as $x) {
142+ if ($x['tag'] !== 0xA3) continue;
143+ foreach (Asn1::children(Asn1::children($x)[0]) as $e) {
144+ $ec = Asn1::children($e);
145+ $oid = Asn1::toOid($ec[0]);
146+ $crit = count($ec) === 3 && $ec[1]['tag'] === Asn1::BOOLEAN && $ec[1]['value'] !== "\0";
147+ if (isset($ext[$oid])) throw new \UnexpectedValueException("X509:重複した拡張:{$oid}");
148+ $ext[$oid] = ['critical' => $crit, 'value' => end($ec)['value']];
149+ }
150+ }
151+ $this->extensions = $ext;
152+ }
153+
154+ /**
155+ * PEMから読む(最初の証明書)
156+ *
157+ * @throws \UnexpectedValueException
158+ */
159+ public static function fromPem(string $pem): self {
160+ foreach (Asn1::unpem($pem) as $b) {
161+ if ($b['label'] === 'CERTIFICATE') return new self($b['der']);
162+ }
163+ throw new \UnexpectedValueException('X509:証明書が見つからない');
164+ }
165+
166+ /**
167+ * PEMの中の証明書を全部読む(チェーン)
168+ *
169+ * @return self[]
170+ */
171+ public static function allFromPem(string $pem): array {
172+ $out = [];
173+ foreach (Asn1::unpem($pem) as $b) {
174+ if ($b['label'] === 'CERTIFICATE') $out[] = new self($b['der']);
175+ }
176+ return $out;
177+ }
178+
179+ public function pem(): string {
180+ return Asn1::pem($this->der, 'CERTIFICATE');
181+ }
182+
183+ /** 公開鍵 */
184+ public function publicKey(): Rsa|Ec {
185+ $alg = Asn1::toOid(Asn1::children(Asn1::children(Asn1::parse($this->spki))[0])[0]);
186+ return match ($alg) {
187+ Rsa::OID, Rsa::OID_PSS => Rsa::fromDer($this->spki),
188+ Ec::OID => Ec::fromDer($this->spki),
189+ default => throw new \UnexpectedValueException("X509:未対応の鍵:{$alg}"),
190+ };
191+ }
192+
193+ /**
194+ * この証明書の署名を発行者の鍵で確認する
195+ *
196+ * @param Rsa|Ec $key 発行者の公開鍵
197+ * @param bool $allowSha1 SHA-1を許すかどうか
198+ * @return bool
199+ */
200+ public function verifySignature(Rsa|Ec $key, bool $allowSha1 = false): bool {
201+ [$type, $hash] = self::SIG_ALGS[$this->sigAlg] ?? [null, null];
202+ if ($type === null) return false;
203+
204+ if ($type === 'pss') {
205+ [$hash, $salt] = self::pssParams($this->sigParams[0] ?? null);
206+ if ($hash === null || !($key instanceof Rsa)) return false;
207+ return $key->verifyPss($this->tbs, $this->signature, $hash, $salt);
208+ }
209+ if ($hash === 'sha1' && !$allowSha1) return false;
210+ if ($type === 'rsa') return $key instanceof Rsa && $key->bits >= 1024 && $key->verify($this->tbs, $this->signature, $hash);
211+ return $key instanceof Ec && $key->verify($this->tbs, $this->signature, $hash);
212+ }
213+
214+ /** RSASSA-PSS-params からハッシュとソルトの長さ */
215+ private static function pssParams(?array $node): array {
216+ $hash = 'sha1';
217+ $salt = 20;
218+ if ($node === null) return [$hash, $salt];
219+ foreach (Asn1::children($node) as $p) {
220+ $v = Asn1::children($p)[0] ?? null;
221+ if ($v === null) continue;
222+ if ($p['tag'] === 0xA0) $hash = self::HASH_OIDS[Asn1::toOid(Asn1::children($v)[0])] ?? null;
223+ if ($p['tag'] === 0xA1) {
224+ $mgf = Asn1::children($v);
225+ $mgfHash = isset($mgf[1]) ? (self::HASH_OIDS[Asn1::toOid(Asn1::children($mgf[1])[0])] ?? null) : 'sha1';
226+ if (Asn1::toOid($mgf[0]) !== '1.2.840.113549.1.1.8' || $mgfHash !== $hash) return [null, 0];
227+ }
228+ if ($p['tag'] === 0xA2) $salt = gmp_intval(Asn1::toInt($v));
229+ }
230+ if ($hash === 'sha1') return [null, 0];
231+ return [$hash, $salt];
232+ }
233+
234+ ///////////////
235+ // 拡張
236+
237+ /** subjectAltNameのdNSNameとiPAddress */
238+ public function altNames(): array {
239+ $out = ['dns' => [], 'ip' => []];
240+ if (!isset($this->extensions['2.5.29.17'])) return $out;
241+ foreach (Asn1::children(Asn1::parse($this->extensions['2.5.29.17']['value'])) as $n) {
242+ if ($n['tag'] === 0x82) $out['dns'][] = strtolower($n['value']);
243+ if ($n['tag'] === 0x87) $out['ip'][] = $n['value'];
244+ }
245+ return $out;
246+ }
247+
248+ /** 主体の属性(CN等)の値 */
249+ public function subjectAttr(string $name): array {
250+ if ($this->subjectAttrs === null) {
251+ $this->subjectAttrs = [];
252+ foreach (Asn1::children(Asn1::parse($this->subjectRaw)) as $rdn) {
253+ foreach (Asn1::children($rdn) as $atv) {
254+ $a = Asn1::children($atv);
255+ $this->subjectAttrs[self::NAMES[Asn1::toOid($a[0])] ?? Asn1::toOid($a[0])][] = Asn1::toString($a[1]);
256+ }
257+ }
258+ }
259+ return $this->subjectAttrs[$name] ?? [];
260+ }
261+
262+ /** basicConstraints:[CAかどうか, pathLen(無ければnull)] */
263+ public function basicConstraints(): array {
264+ if (!isset($this->extensions['2.5.29.19'])) return [false, null];
265+ $c = Asn1::children(Asn1::parse($this->extensions['2.5.29.19']['value']));
266+ $ca = isset($c[0]) && $c[0]['tag'] === Asn1::BOOLEAN && $c[0]['value'] !== "\0";
267+ $len = null;
268+ foreach ($c as $x) if ($x['tag'] === Asn1::INTEGER) $len = gmp_intval(Asn1::toInt($x));
269+ return [$ca, $len];
270+ }
271+
272+ /** keyUsageのビット(0=digitalSignature、5=keyCertSign)、拡張が無ければnull */
273+ public function keyUsage(): ?array {
274+ if (!isset($this->extensions['2.5.29.15'])) return null;
275+ $bits = Asn1::toBits(Asn1::parse($this->extensions['2.5.29.15']['value']));
276+ $out = [];
277+ for ($i = 0; $i < 8 * strlen($bits); $i++) {
278+ if (ord($bits[$i >> 3]) & (0x80 >> ($i & 7))) $out[] = $i;
279+ }
280+ return $out;
281+ }
282+
283+ /** extendedKeyUsageのOID、拡張が無ければnull */
284+ public function extKeyUsage(): ?array {
285+ if (!isset($this->extensions['2.5.29.37'])) return null;
286+ return array_map([Asn1::class, 'toOid'], Asn1::children(Asn1::parse($this->extensions['2.5.29.37']['value'])));
287+ }
288+
289+ public function isSelfIssued(): bool {
290+ return $this->issuerRaw === $this->subjectRaw;
291+ }
292+
293+ ///////////////
294+ // 確認
295+
296+ /**
297+ * ホスト名(又はIPアドレス)がこの証明書に合うかどうか(RFC 6125)
298+ *
299+ * subjectAltNameが無い場合だけ、CNを見る。ワイルドカードは一番左のラベルの「*」だけ。
300+ *
301+ * @param string $host
302+ * @return bool
303+ */
304+ public function matchesHost(string $host): bool {
305+ $host = strtolower(rtrim(trim($host, '[]'), '.'));
306+ $alt = $this->altNames();
307+ $ip = @inet_pton($host);
308+
309+ if ($ip !== false) {
310+ foreach ($alt['ip'] as $a) if ($a === $ip) return true;
311+ if (empty($alt['ip']) && empty($alt['dns'])) {
312+ foreach ($this->subjectAttr('CN') as $cn) if (@inet_pton($cn) === $ip) return true;
313+ }
314+ return false;
315+ }
316+
317+ $names = $alt['dns'] ?: (empty($alt['ip']) ? array_map('strtolower', $this->subjectAttr('CN')) : []);
318+ foreach ($names as $n) {
319+ $n = rtrim($n, '.');
320+ if ($n === $host) return true;
321+ if (str_starts_with($n, '*.') && substr_count($n, '.') >= 2) {
322+ $dot = strpos($host, '.');
323+ if ($dot !== false && $dot > 0 && substr($host, $dot) === substr($n, 1)) return true;
324+ }
325+ }
326+ return false;
327+ }
328+
329+ /**
330+ * サーバーの証明書のチェーンを確認する
331+ *
332+ * @param self[] $chain サーバーから受け取った証明書([0]がサーバーの証明書)
333+ * @param string|null $host 確認するホスト名(nullなら確認しない)
334+ * @param array $opt cafile(信頼する証明書のPEMファイル)、trusted(self[]、cafileの代わり)、
335+ * allow_self_signed(自己署名の証明書を許す)、time(確認する時間)
336+ * @return void
337+ * @throws \RuntimeException 確認に失敗した場合(理由はメッセージ)
338+ */
339+ public static function verifyChain(array $chain, ?string $host, array $opt = []): void {
340+ if (empty($chain)) throw new \RuntimeException('証明書がありません');
341+ $now = (int)($opt['time'] ?? time());
342+ $leaf = $chain[0];
343+
344+ if ($now < $leaf->notBefore) throw new \RuntimeException("証明書はまだ有効ではありません({$leaf->subject})");
345+ if ($now > $leaf->notAfter) throw new \RuntimeException("証明書の有効期限が切れています({$leaf->subject})");
346+ if ($host !== null && !$leaf->matchesHost($host)) throw new \RuntimeException("証明書のホスト名が一致しません({$host}、{$leaf->subject})");
347+ $eku = $leaf->extKeyUsage();
348+ if ($eku !== null && !in_array(self::EKU_SERVER, $eku, true) && !in_array(self::EKU_ANY, $eku, true)) {
349+ throw new \RuntimeException('証明書はサーバー用ではありません');
350+ }
351+ self::checkCritical($leaf);
352+
353+ if (!empty($opt['allow_self_signed']) && $leaf->isSelfIssued()) {
354+ if ($leaf->verifySignature($leaf->publicKey(), true)) return;
355+ }
356+
357+ $trusted = $opt['trusted'] ?? self::store($opt['cafile'] ?? null);
358+ $names = $leaf->altNames();
359+ if (empty($names['dns']) && empty($names['ip'])) $names['dns'] = array_map('strtolower', $leaf->subjectAttr('CN'));
360+
361+ $cur = $leaf;
362+ $used = [0 => true];
363+ for ($depth = 0; $depth <= self::MAX_DEPTH; $depth++) {
364+ // 信頼する証明書そのもの
365+ foreach ($trusted[$cur->subjectRaw] ?? [] as $t) {
366+ if ($t->der === $cur->der) return;
367+ }
368+ // 信頼する証明書が発行した
369+ foreach ($trusted[$cur->issuerRaw] ?? [] as $t) {
370+ if ($now > $t->notAfter || $now < $t->notBefore) continue;
371+ if ($cur->verifySignature($t->publicKey())) {
372+ self::checkNameConstraints($t, $names);
373+ return;
374+ }
375+ }
376+ // 受け取った中間証明書
377+ $next = null;
378+ foreach ($chain as $i => $c) {
379+ if (isset($used[$i]) || $c->subjectRaw !== $cur->issuerRaw) continue;
380+ if (!$cur->verifySignature($c->publicKey())) continue;
381+ $next = $i;
382+ break;
383+ }
384+ if ($next === null) {
385+ throw new \RuntimeException($cur->isSelfIssued() ? "自己署名の証明書です({$cur->subject})" : "信頼出来ない証明書です(発行者:{$cur->issuer})");
386+ }
387+ $ca = $chain[$next];
388+ [$isCa, $pathLen] = $ca->basicConstraints();
389+ if (!$isCa) throw new \RuntimeException("中間証明書がCAではありません({$ca->subject})");
390+ if ($pathLen !== null && $depth > $pathLen) throw new \RuntimeException("中間証明書のpathLenを超えました({$ca->subject})");
391+ $ku = $ca->keyUsage();
392+ if ($ku !== null && !in_array(5, $ku, true)) throw new \RuntimeException("中間証明書は証明書に署名出来ません({$ca->subject})");
393+ if ($now > $ca->notAfter || $now < $ca->notBefore) throw new \RuntimeException("中間証明書の有効期限が切れています({$ca->subject})");
394+ self::checkCritical($ca);
395+ self::checkNameConstraints($ca, $names);
396+ $used[$next] = true;
397+ $cur = $ca;
398+ }
399+ throw new \RuntimeException('証明書のチェーンが長過ぎます');
400+ }
401+
402+ /** 理解していない重要な拡張があれば拒否する */
403+ private static function checkCritical(self $c): void {
404+ foreach ($c->extensions as $oid => $e) {
405+ if ($e['critical'] && !in_array($oid, self::KNOWN_EXT, true)) throw new \RuntimeException("未対応の重要な拡張:{$oid}");
406+ }
407+ }
408+
409+ /** nameConstraints(dNSNameとiPAddressだけ) */
410+ private static function checkNameConstraints(self $ca, array $names): void {
411+ if (!isset($ca->extensions['2.5.29.30'])) return;
412+ $sets = ['permitted' => ['dns' => [], 'ip' => []], 'excluded' => ['dns' => [], 'ip' => []]];
413+ foreach (Asn1::children(Asn1::parse($ca->extensions['2.5.29.30']['value'])) as $part) {
414+ $kind = $part['tag'] === 0xA0 ? 'permitted' : 'excluded';
415+ foreach (Asn1::children($part) as $subtree) {
416+ $base = Asn1::children($subtree)[0];
417+ if ($base['tag'] === 0x82) $sets[$kind]['dns'][] = strtolower(ltrim($base['value'], '.'));
418+ if ($base['tag'] === 0x87) $sets[$kind]['ip'][] = $base['value'];
419+ }
420+ }
421+ $dnsIn = fn(string $n, string $c): bool => $c === '' || $n === $c || str_ends_with($n, '.'.$c);
422+ $ipIn = function(string $ip, string $c): bool {
423+ $l = strlen($ip);
424+ if (strlen($c) !== 2 * $l) return false;
425+ return ($ip & substr($c, $l)) === (substr($c, 0, $l) & substr($c, $l));
426+ };
427+ foreach (['dns' => $dnsIn, 'ip' => $ipIn] as $type => $in) {
428+ foreach ($names[$type] as $n) {
429+ $n = $type === 'dns' ? ltrim($n, '*.') : $n;
430+ foreach ($sets['excluded'][$type] as $c) if ($in($n, $c)) throw new \RuntimeException("名前の制約に違反します({$ca->subject})");
431+ if (!empty($sets['permitted'][$type])) {
432+ $ok = false;
433+ foreach ($sets['permitted'][$type] as $c) if ($in($n, $c)) $ok = true;
434+ if (!$ok) throw new \RuntimeException("名前の制約に違反します({$ca->subject})");
435+ }
436+ }
437+ }
438+ }
439+
440+ /**
441+ * 信頼する証明書を読む(主体のDER => self[]、プロセスの中でキャッシュする)
442+ *
443+ * @param string|null $file PEMファイル(nullならTLS_CAFILE、無ければCA_FILESを探す)
444+ * @return array
445+ * @throws \RuntimeException ファイルが見つからない場合
446+ */
447+ public static function store(?string $file = null): array {
448+ if ($file === null || $file === '') {
449+ $file = defined('TLS_CAFILE') && TLS_CAFILE !== '' ? (string)TLS_CAFILE : null;
450+ if ($file === null) {
451+ foreach (self::CA_FILES as $f) {
452+ if (@is_file($f)) {
453+ $file = $f;
454+ break;
455+ }
456+ }
457+ }
458+ if ($file === null) throw new \RuntimeException('信頼する証明書のファイルが見つかりません(TLS_CAFILEを設定して下さい)');
459+ }
460+ if (isset(self::$stores[$file])) return self::$stores[$file];
461+
462+ $pem = @file_get_contents($file);
463+ if ($pem === false) throw new \RuntimeException("信頼する証明書のファイルを読めません:{$file}");
464+ $out = [];
465+ foreach (Asn1::unpem($pem) as $b) {
466+ if ($b['label'] !== 'CERTIFICATE' && $b['label'] !== 'TRUSTED CERTIFICATE') continue;
467+ try {
468+ $c = new self($b['der']);
469+ } catch (\Throwable $e) {
470+ continue;
471+ }
472+ $out[$c->subjectRaw][] = $c;
473+ }
474+ return self::$stores[$file] = $out;
475+ }
476+
477+ /**
478+ * 証明書の配列を、verifyChain()のtrustedの形にする
479+ *
480+ * @param self[] $certs
481+ * @return array
482+ */
483+ public static function trust(array $certs): array {
484+ $out = [];
485+ foreach ($certs as $c) $out[$c->subjectRaw][] = $c;
486+ return $out;
487+ }
488+
489+ ///////////////
490+ // 作成
491+
492+ /**
493+ * 証明書を作る(テスト、MySQL等の内部のサーバー用)
494+ *
495+ * @param Rsa|Ec $key この証明書の鍵(自己署名なら秘密鍵)
496+ * @param string $cn 主体のCN
497+ * @param array $opt sans(DNS名・IPアドレスの配列、省略するとCN)、days(365)、notBefore、notAfter、
498+ * ca(bool)、pathLen、issuer([self 発行者の証明書, Rsa|Ec 発行者の秘密鍵]、省略すると自己署名)
499+ * @return self
500+ */
501+ public static function issue(Rsa|Ec $key, string $cn, array $opt = []): self {
502+ [$issuerCert, $issuerKey] = $opt['issuer'] ?? [null, $key];
503+ if (!$issuerKey->isPrivate()) throw new \InvalidArgumentException('X509:署名に秘密鍵が必要');
504+
505+ $name = Asn1::seq(Asn1::set(Asn1::seq(Asn1::oid('2.5.4.3'), Asn1::utf8($cn))));
506+ $notBefore = $opt['notBefore'] ?? time() - 300;
507+ $notAfter = $opt['notAfter'] ?? $notBefore + 86400 * ($opt['days'] ?? 365);
508+
509+ $sans = '';
510+ foreach ($opt['sans'] ?? [$cn] as $s) {
511+ $ip = @inet_pton($s);
512+ $sans .= $ip !== false ? Asn1::tlv(0x87, $ip) : Asn1::tlv(0x82, $s);
513+ }
514+ $isCa = !empty($opt['ca']);
515+ $bc = $isCa ? Asn1::seq(Asn1::bool(true), ...(isset($opt['pathLen']) ? [Asn1::int($opt['pathLen'])] : [])) : Asn1::seq();
516+ $ku = $isCa ? "\x03\x02\x01\x06" : "\x03\x02\x05\xA0"; // keyCertSign・cRLSign、又はdigitalSignature・keyEncipherment
517+ $exts = Asn1::seq(Asn1::oid('2.5.29.19'), Asn1::bool(true), Asn1::octets($bc))
518+ .Asn1::seq(Asn1::oid('2.5.29.15'), Asn1::bool(true), Asn1::octets($ku))
519+ .($isCa ? '' : Asn1::seq(Asn1::oid('2.5.29.17'), Asn1::octets(Asn1::seq($sans))));
520+
521+ [$algOid, $hash] = $issuerKey instanceof Rsa
522+ ? ['1.2.840.113549.1.1.11', 'sha256']
523+ : ($issuerKey->curve === 'P-384' ? ['1.2.840.10045.4.3.3', 'sha384'] : ['1.2.840.10045.4.3.2', 'sha256']);
524+ $alg = $issuerKey instanceof Rsa ? Asn1::seq(Asn1::oid($algOid), Asn1::null()) : Asn1::seq(Asn1::oid($algOid));
525+
526+ $serial = gmp_import(random_bytes(16));
527+ $tbs = Asn1::seq(
528+ Asn1::explicit(0, Asn1::int(2)),
529+ Asn1::int($serial),
530+ $alg,
531+ $issuerCert ? $issuerCert->subjectRaw : $name,
532+ Asn1::seq(Asn1::time($notBefore), Asn1::time($notAfter)),
533+ $name,
534+ $key->publicDer(),
535+ Asn1::explicit(3, Asn1::seq($exts)),
536+ );
537+ return new self(Asn1::seq($tbs, $alg, Asn1::bits($issuerKey->sign($tbs, $hash))));
538+ }
539+
540+ /** 名前を「CN=…, O=…」の文字列にする */
541+ private static function nameString(array $node): string {
542+ $parts = [];
543+ foreach (Asn1::children($node) as $rdn) {
544+ foreach (Asn1::children($rdn) as $atv) {
545+ $a = Asn1::children($atv);
546+ $oid = Asn1::toOid($a[0]);
547+ $parts[] = (self::NAMES[$oid] ?? $oid).'='.Asn1::toString($a[1]);
548+ }
549+ }
550+ return implode(', ', $parts);
551+ }
552+}
@@ -502,26 +502,25 @@
502502 $errstr = '';
503503
504504 if ($scheme === 'https') {
505+ // PHP純正のTLS 1.3・1.2(Std\Lib\Tls)
505506 $sslOptions = [
506507 'verify_peer' => $this->verifySSL,
507508 'verify_peer_name' => $this->verifySSL,
508509 'peer_name' => trim($host, '[]'),
510+ 'alpn' => ['http/1.1'],
509511 ];
510512
511513 if (!empty($this->caInfoPath) && file_exists($this->caInfoPath)) {
512514 $sslOptions['cafile'] = $this->caInfoPath;
513515 }
514516
515- $context = stream_context_create(['ssl' => $sslOptions]);
516-
517- $socket = @stream_socket_client(
518- "tls://{$connectHost}:{$port}",
519- $errno,
520- $errstr,
521- $this->timeout,
522- STREAM_CLIENT_CONNECT,
523- $context
524- );
517+ try {
518+ $socket = Tls::connect($connectHost, (int)$port, $sslOptions, $this->timeout);
519+ } catch (Tls\TlsException $e) {
520+ $socket = false;
521+ $errstr = $e->getMessage();
522+ $errno = $e->alert;
523+ }
525524 } else {
526525 $socket = @fsockopen($connectHost, $port, $errno, $errstr, $this->timeout);
527526 }
@@ -559,13 +558,16 @@
559558 $readError = "レスポンスが大き過ぎます(上限:{$this->maxResponseSize}バイト)";
560559 break;
561560 }
562- if (stream_get_meta_data($socket)['timed_out'] || microtime(true) > $deadline) {
561+ if (stream_get_meta_data($socket)['timed_out'] || Tls::timedOut($socket) || microtime(true) > $deadline) {
563562 $readError = "タイムアウト({$this->timeout}秒)";
564563 break;
565564 }
566565 if ($chunk === false) break;
567566 }
568567
568+ // 読み込み中のTLSのエラー(改ざん等)
569+ $tlsError = Tls::error($socket);
570+ if ($readError === '' && $tlsError !== null) $readError = $tlsError->getMessage();
569571 fclose($socket);
570572
571573 if ($readError !== '') {
@@ -37,6 +37,10 @@
3737 */
3838 namespace Std\Lib\Db;
3939
40+use Std\Lib\Crypto\Rsa;
41+use Std\Lib\Tls;
42+use Std\Lib\Tls\TlsException;
43+
4044 /**
4145 * PHP純正のMySQL(MariaDBも)ドライバ。mysqli・pdo_mysql・socketsのモジュールが不要。
4246 *
@@ -159,7 +163,7 @@
159163 *
160164 * @param array|null $info host、port(3306)、socket(UNIXソケットのパス、指定するとhost・portの代わりに使う)、
161165 * username、password、dbname、charset(utf8mb4)、
162- * tls(true=証明書を確認するTLS、又はstream_context_createのsslオプションの配列)、
166+ * tls(true=証明書を確認するTLS、又はStd\Lib\Tlsのオプションの配列:cafile、verify_peer等)、
163167 * timeout(秒、10)、pubkey(caching_sha2_password・sha256_password用のサーバーの公開鍵のPEMファイル、任意)、debug
164168 * @throws \Exception $infoを省略し、DB_ENABLEDが無効な場合
165169 */
@@ -714,31 +718,13 @@
714718 */
715719 private function startTls(): void {
716720 $opts = is_array($this->tls) ? $this->tls : [];
717- $opts += [
718- 'verify_peer' => true,
719- 'verify_peer_name' => true,
720- 'peer_name' => $this->host,
721- 'SNI_enabled' => true,
722- ];
723- stream_context_set_option($this->socket, ['ssl' => $opts]);
721+ $opts += ['peer_name' => $this->host];
724722
725- $method = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
726- if (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT')) $method |= STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT;
727-
728- $err = '';
729- set_error_handler(function(int $no, string $str) use (&$err): bool {
730- $err = $str;
731- return true;
732- });
733723 try {
734- $ok = stream_socket_enable_crypto($this->socket, true, $method);
735- } finally {
736- restore_error_handler();
724+ $this->socket = Tls::enable($this->socket, $opts);
725+ } catch (TlsException $e) {
726+ $this->fail('TLSの開始に失敗: '.$e->getMessage());
737727 }
738-
739- if ($ok !== true) {
740- $this->fail('TLSの開始に失敗'.($err !== '' ? ": {$err}" : ''));
741- }
742728 $this->secure = true;
743729 }
744730
@@ -751,10 +737,6 @@
751737 * @throws DbException
752738 */
753739 private function rsaEncrypt(string $pem, string $scramble): string {
754- if (!function_exists('openssl_public_encrypt')) {
755- $this->fail('パスワードの暗号化にはopensslモジュールが必要です(又はTLSで接続して下さい)');
756- }
757-
758740 $pw = $this->password."\0";
759741 $xor = '';
760742 $len = strlen($scramble);
@@ -762,12 +744,12 @@
762744 $xor .= $pw[$i] ^ $scramble[$i % $len];
763745 }
764746
765- $key = openssl_pkey_get_public($pem);
766- if ($key === false || !openssl_public_encrypt($xor, $out, $key, OPENSSL_PKCS1_OAEP_PADDING)) {
767- $this->fail('パスワードの暗号化に失敗');
747+ // OAEP(SHA-1、MySQLのRSA_PKCS1_OAEP_PADDINGと同じ)
748+ try {
749+ return Rsa::fromPem($pem)->encrypt($xor, 'sha1');
750+ } catch (\InvalidArgumentException $e) {
751+ $this->fail('パスワードの暗号化に失敗: '.$e->getMessage());
768752 }
769-
770- return $out;
771753 }
772754
773755 /**
@@ -1199,7 +1181,7 @@
11991181 while (strlen($buf) < $len) {
12001182 $chunk = @fread($this->socket, min($len - strlen($buf), 1048576));
12011183 if ($chunk === false || $chunk === '') {
1202- $timedOut = (bool)(stream_get_meta_data($this->socket)['timed_out'] ?? false);
1184+ $timedOut = Tls::timedOut($this->socket) || (bool)(stream_get_meta_data($this->socket)['timed_out'] ?? false);
12031185 if (!$timedOut && $chunk === '' && !feof($this->socket)) continue;
12041186 $this->drop();
12051187 $this->fail($timedOut ? "パケットの読み込みがタイムアウト({$this->timeout}秒)" : 'サーバーとの接続が切れました');
@@ -37,6 +37,8 @@
3737 */
3838 namespace Std\Lib\Fedi;
3939
40+use Std\Lib\Crypto\Rsa;
41+
4042 /**
4143 * ローカルのユーザーのアクター(鍵、プロフィール、WebFinger、コレクション)
4244 */
@@ -44,30 +46,19 @@
4446 /**
4547 * ユーザーの秘密鍵(無ければ作る)
4648 *
47- * @return \OpenSSLAsymmetricKey|null 作れない場合はnull(OpenSSLの設定が無い等)
49+ * @return Rsa|null 作れない・読めない場合はnull
4850 */
49- public static function privateKey(string $username): ?\OpenSSLAsymmetricKey {
51+ public static function privateKey(string $username): ?Rsa {
5052 $file = Fedi::userPath($username, 'private.pem');
5153 if (!is_file($file)) {
5254 $lock = fopen($file.'.lock', 'c');
5355 if ($lock) flock($lock, LOCK_EX);
5456 try {
5557 if (!is_file($file)) {
56- $opts = ['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA];
57- $key = @openssl_pkey_new($opts);
58- if ($key === false) {
59- $cnf = Fedi::path('openssl.cnf');
60- if (!is_file($cnf)) file_put_contents($cnf, "# LittleBeastが鍵を作る時に使う(chrootでは/etc/ssl/openssl.cnfが見えない為)\n");
61- $opts['config'] = $cnf;
62- $key = @openssl_pkey_new($opts);
63- }
64- if ($key === false || !openssl_pkey_export($key, $pem, null, $opts)) {
65- logger(\LogType::ActivityPub, "「{$username}」の鍵を作れませんでした:".openssl_error_string());
66- return null;
67- }
68- file_put_contents($file, $pem);
58+ $key = Rsa::generate(2048);
59+ file_put_contents($file, $key->toPem());
6960 @chmod($file, 0600);
70- file_put_contents(Fedi::userPath($username, 'public.pem'), openssl_pkey_get_details($key)['key']);
61+ file_put_contents(Fedi::userPath($username, 'public.pem'), $key->publicPem());
7162 }
7263 } finally {
7364 if ($lock) {
@@ -76,8 +67,13 @@
7667 }
7768 }
7869 }
79- $key = openssl_pkey_get_private((string)file_get_contents($file));
80- return $key === false ? null : $key;
70+ try {
71+ $key = Rsa::fromPem((string)file_get_contents($file));
72+ } catch (\InvalidArgumentException $e) {
73+ logger(\LogType::ActivityPub, "「{$username}」の鍵を読めませんでした:".$e->getMessage());
74+ return null;
75+ }
76+ return $key->isPrivate() ? $key : null;
8177 }
8278
8379 public static function publicKeyPem(string $username): ?string {
@@ -39,6 +39,7 @@
3939
4040 use LogType;
4141 use Result;
42+use Std\Lib\Tls\TlsException;
4243
4344 class Mailer {
4445 private $socket;
@@ -51,6 +52,8 @@
5152 private ?string $pgpKey = null;
5253 private ?string $pgpPass = null;
5354 private int $timeout = 30;
55+ /** STARTTLSのオプション(Std\Lib\Tlsを参照、例:['cafile' => …]) */
56+ private array $tls = [];
5457
5558 /**
5659 * コンストラクタ
@@ -68,6 +71,7 @@
6871 $this->user = $info['user'] ?? null;
6972 $this->pass = $info['pass'] ?? null;
7073 $this->from = (string)($info['from'] ?? '');
74+ $this->tls = (array)($info['tls'] ?? []);
7175 }
7276
7377 /**
@@ -163,10 +167,11 @@
163167 if (strpos($ehloRes, 'STARTTLS') !== false) {
164168 $this->sendCommand('STARTTLS', 220);
165169
166- // TLS暗号化
167- if (!stream_socket_enable_crypto(
168- $this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
169- $msg = "TLSハンドシェイクに失敗";
170+ // TLS暗号化(TLS 1.3・1.2、証明書はサーバーのホスト名で確認する)
171+ try {
172+ $this->socket = Tls::enable($this->socket, $this->tls + ['peer_name' => $this->host]);
173+ } catch (TlsException $e) {
174+ $msg = "TLSハンドシェイクに失敗: ".$e->getMessage();
170175 logger(LogType::Mailer, $msg);
171176 throw new \Exception($msg);
172177 }
@@ -0,0 +1,165 @@
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;
39+
40+use Std\Lib\Tls\Connection;
41+use Std\Lib\Tls\Stream;
42+use Std\Lib\Tls\TlsException;
43+
44+/**
45+ * PHP純正のTLS 1.3・1.2(php-opensslが不要、GMPは必要)
46+ *
47+ * 結果はPHPのストリームなので、fread・fgets・fwrite・feof・fclose・stream_set_timeoutがそのまま使える。
48+ *
49+ * クライアント:
50+ * $s = Tls::connect('example.com', 443, ['alpn' => ['http/1.1']]);
51+ * fwrite($s, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
52+ * STARTTLS(既に接続しているソケット):
53+ * $s = Tls::enable($socket, ['peer_name' => 'smtp.example.com']);
54+ * サーバー(テスト等):
55+ * $s = Tls::accept($client, ['cert' => $certPem, 'key' => $keyPem]);
56+ *
57+ * オプション(PHPのsslコンテキストと同じ名前):
58+ * peer_name 確認するホスト名(SNIにも使う)
59+ * verify_peer 証明書を確認する(既定true)
60+ * verify_peer_name ホスト名を確認する(既定true)
61+ * allow_self_signed 自己署名の証明書を許す(既定false)
62+ * cafile 信頼する証明書のPEMファイル(省略するとTLS_CAFILE、又はシステムの証明書)
63+ * SNI_enabled SNIを送る(既定true)
64+ * alpn ALPNのプロトコルの配列(例:['http/1.1'])
65+ * groups 鍵交換のグループ(既定['x25519', 'P-256', 'P-384']、最初の鍵を最初に送る)
66+ * min_version・max_version 使うバージョン('1.2'又は'1.3'、既定は1.2から1.3)
67+ * ciphers 暗号スイートの番号の配列(Connection::SUITES・SUITES12のキー、既定は全部)
68+ * cert・key サーバーの証明書(チェーン)と秘密鍵(PEM又はファイル、local_cert・local_pkも可)
69+ */
70+class Tls {
71+ /**
72+ * TCPで接続してTLSを開始する
73+ *
74+ * @param string $host ホスト名又はIPアドレス
75+ * @param int $port ポート
76+ * @param array $opt オプション(peer_nameを省略すると$host)
77+ * @param float $timeout 接続と読み込みのタイムアウト(秒)
78+ * @return resource ストリーム
79+ * @throws TlsException 接続又はハンドシェイクに失敗した場合
80+ */
81+ public static function connect(string $host, int $port, array $opt = [], float $timeout = 10) {
82+ $addr = str_contains($host, ':') && !str_starts_with($host, '[') ? "[{$host}]" : $host;
83+ $errno = 0;
84+ $errstr = '';
85+ $sock = @stream_socket_client("tcp://{$addr}:{$port}", $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, stream_context_create());
86+ if ($sock === false) throw new TlsException("接続出来ません:{$errstr}({$errno})", Connection::ALERT_INTERNAL_ERROR, true);
87+ stream_set_timeout($sock, (int)$timeout, (int)(fmod($timeout, 1) * 1000000));
88+ $opt += ['peer_name' => trim($host, '[]')];
89+ try {
90+ return self::enable($sock, $opt);
91+ } catch (\Throwable $e) {
92+ @fclose($sock);
93+ throw $e;
94+ }
95+ }
96+
97+ /**
98+ * 既に接続しているソケットでTLSを開始する(クライアント、STARTTLS等)
99+ *
100+ * @param resource $socket ソケット(返したストリームを閉じると、これも閉じる)
101+ * @param array $opt オプション
102+ * @return resource ストリーム
103+ * @throws TlsException
104+ */
105+ public static function enable($socket, array $opt = []) {
106+ $conn = new Connection($socket, $opt, false);
107+ $conn->handshake();
108+ return Stream::wrap($conn);
109+ }
110+
111+ /**
112+ * 既に接続しているソケットでTLSを開始する(サーバー)
113+ *
114+ * @param resource $socket 受け付けたソケット
115+ * @param array $opt オプション(cert・keyが必要)
116+ * @return resource ストリーム
117+ * @throws TlsException
118+ */
119+ public static function accept($socket, array $opt) {
120+ $conn = new Connection($socket, $opt, true);
121+ $conn->handshake();
122+ return Stream::wrap($conn);
123+ }
124+
125+ /**
126+ * TLSのストリームの情報
127+ *
128+ * @param resource $stream
129+ * @return array|null version、cipher、group、alpn、peer_certificates、server_name、TLSでなければnull
130+ */
131+ public static function info($stream): ?array {
132+ return Stream::connection($stream)?->info();
133+ }
134+
135+ /**
136+ * 読み込み中のエラー(feof()がtrueになった理由)
137+ *
138+ * @param resource $stream
139+ * @return TlsException|null
140+ */
141+ public static function error($stream): ?TlsException {
142+ return Stream::connection($stream)?->lastError();
143+ }
144+
145+ /**
146+ * 読んでいないデータが残っているかどうか(stream_select()を使うサーバー用:TLSの中に溜まったデータは見えない)
147+ *
148+ * @param resource $stream
149+ * @return bool
150+ */
151+ public static function pending($stream): bool {
152+ if (!is_resource($stream)) return false;
153+ return (stream_get_meta_data($stream)['unread_bytes'] ?? 0) > 0 || (bool)Stream::connection($stream)?->pending();
154+ }
155+
156+ /**
157+ * 最後の読み込みがタイムアウトしたかどうか
158+ *
159+ * @param resource $stream
160+ * @return bool
161+ */
162+ public static function timedOut($stream): bool {
163+ return (bool)Stream::connection($stream)?->timedOut();
164+ }
165+}
@@ -0,0 +1,1516 @@
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+}
@@ -0,0 +1,125 @@
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+/**
41+ * TLSの接続をPHPのストリームにする(fread・fgets・fwrite・feof・fclose・stream_set_timeout・stream_selectが使える)
42+ *
43+ * Std\Lib\Tls::connect()等が作るので、直接使わない。
44+ * 読み込み中のエラーは例外にせず、終わり(feof)として扱う(Std\Lib\Tls::error()で確認出来る)。
45+ */
46+class Stream {
47+ public const SCHEME = 'lbtls';
48+
49+ /** @var resource|null */
50+ public $context;
51+ private ?Connection $conn = null;
52+ private static array $pending = [];
53+
54+ /**
55+ * 接続をストリームにする
56+ *
57+ * @param Connection $conn ハンドシェイク済みの接続
58+ * @return resource
59+ */
60+ public static function wrap(Connection $conn) {
61+ if (!in_array(self::SCHEME, stream_get_wrappers(), true)) stream_wrapper_register(self::SCHEME, self::class);
62+ $id = spl_object_id($conn);
63+ self::$pending[$id] = $conn;
64+ $s = fopen(self::SCHEME.'://'.$id, 'r+');
65+ unset(self::$pending[$id]);
66+ // PHPのバッファで書き込みが遅れない様に
67+ stream_set_write_buffer($s, 0);
68+ // シーク出来ないストリームにする(stream_seekが無いので失敗し、PHPが印を付ける)。
69+ // シーク出来るストリームにfwriteすると、PHPは読み込みのバッファ(まだ読んでいない受信データ)を捨てる
70+ @fseek($s, 0, SEEK_CUR);
71+ return $s;
72+ }
73+
74+ /** ストリームの接続(Tls::info()等) */
75+ public static function connection($stream): ?Connection {
76+ if (!is_resource($stream)) return null;
77+ $w = stream_get_meta_data($stream)['wrapper_data'] ?? null;
78+ return $w instanceof self ? $w->conn : null;
79+ }
80+
81+ public function stream_open(string $path, string $mode, int $options, ?string &$opened): bool {
82+ $id = (int)substr($path, strlen(self::SCHEME) + 3);
83+ $this->conn = self::$pending[$id] ?? null;
84+ return $this->conn !== null;
85+ }
86+
87+ public function stream_read(int $count): string|false {
88+ return $this->conn->read($count);
89+ }
90+
91+ public function stream_write(string $data): int {
92+ try {
93+ return $this->conn->write($data);
94+ } catch (TlsException $e) {
95+ return 0;
96+ }
97+ }
98+
99+ public function stream_eof(): bool {
100+ return $this->conn->eof();
101+ }
102+
103+ public function stream_close(): void {
104+ $this->conn?->close();
105+ }
106+
107+ public function stream_flush(): bool {
108+ return true;
109+ }
110+
111+ public function stream_set_option(int $option, int $arg1, ?int $arg2): bool {
112+ if ($option === STREAM_OPTION_READ_TIMEOUT) return stream_set_timeout($this->conn->socket(), $arg1, (int)$arg2);
113+ if ($option === STREAM_OPTION_BLOCKING) return $arg1 === 1; // ブロッキングのみ
114+ return false;
115+ }
116+
117+ /** stream_select()用に下のソケットを返す */
118+ public function stream_cast(int $castAs) {
119+ return $this->conn->socket();
120+ }
121+
122+ public function stream_stat(): array {
123+ return [];
124+ }
125+}
@@ -0,0 +1,54 @@
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+/**
41+ * TLSのエラー
42+ */
43+class TlsException extends \RuntimeException {
44+ /** アラートの番号(RFC 8446 6) */
45+ public int $alert;
46+ /** 相手から受け取ったアラートかどうか */
47+ public bool $received;
48+
49+ public function __construct(string $message, int $alert = 80, bool $received = false, ?\Throwable $previous = null) {
50+ parent::__construct($message, $alert, $previous);
51+ $this->alert = $alert;
52+ $this->received = $received;
53+ }
54+}
@@ -0,0 +1,99 @@
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\Test\Helper;
39+
40+use Std\Lib\Crypto\Ec;
41+use Std\Lib\Crypto\Rsa;
42+use Std\Lib\Crypto\X509;
43+
44+/**
45+ * テスト用の証明書(CAとそのCAが発行したサーバーの証明書、PHP純正のStd\Lib\Crypto)
46+ *
47+ * 1つのプロセスで1回だけ作り、一時ファイルに保存する(プロセスの終わりに消す)。
48+ *
49+ * 例:
50+ * $c = TestCert::get();
51+ * $srv = MockServer::start('tls-server.php', [$c['cert'], $c['key']]);
52+ * $s = Tls::connect('127.0.0.1', $srv->port, ['peer_name' => 'localhost', 'cafile' => $c['ca']]);
53+ */
54+class TestCert {
55+ private static array $cache = [];
56+
57+ /**
58+ * 証明書のファイル
59+ *
60+ * @param string $type サーバーの鍵:ec(P-256)、rsa(2048ビット)、self(自己署名のP-256)
61+ * @return array ca(CAの証明書)、cert(サーバーの証明書)、key(サーバーの秘密鍵)、pub(サーバーの公開鍵)のパス
62+ */
63+ public static function get(string $type = 'ec'): array {
64+ if (isset(self::$cache[$type])) return self::$cache[$type];
65+
66+ self::$cache['ca'] ??= (function(): array {
67+ $key = Rsa::generate(2048);
68+ $cert = X509::issue($key, 'LB Test CA', ['ca' => true, 'days' => 30]);
69+ return [$key, $cert, self::write('ca.pem', $cert->pem())];
70+ })();
71+ [$caKey, $caCert, $caFile] = self::$cache['ca'];
72+
73+ $key = $type === 'rsa' ? Rsa::generate(2048) : Ec::generate('P-256');
74+ $opt = ['sans' => ['localhost', '127.0.0.1', '::1'], 'days' => 30];
75+ if ($type !== 'self') $opt['issuer'] = [$caCert, $caKey];
76+ $cert = X509::issue($key, 'localhost', $opt);
77+
78+ return self::$cache[$type] = [
79+ 'ca' => $caFile,
80+ 'cert' => self::write("{$type}-cert.pem", $cert->pem()),
81+ 'key' => self::write("{$type}-key.pem", $key->toPem()),
82+ 'pub' => self::write("{$type}-pub.pem", $key->publicPem()),
83+ ];
84+ }
85+
86+ private static function write(string $name, string $data): string {
87+ static $dir = null;
88+ if ($dir === null) {
89+ $dir = sys_get_temp_dir().'/lbcert_'.bin2hex(random_bytes(6));
90+ mkdir($dir, 0700);
91+ register_shutdown_function(function() use (&$dir): void {
92+ foreach (glob($dir.'/*') ?: [] as $f) @unlink($f);
93+ @rmdir($dir);
94+ });
95+ }
96+ file_put_contents("{$dir}/{$name}", $data);
97+ return "{$dir}/{$name}";
98+ }
99+}
@@ -35,10 +35,15 @@
3535 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
3636 OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
3737 */
38-// テスト用の偽物のHTTPサーバー(MockServerが起動する)。使い方:php http-server.php ポート
39-// 1回に1つの接続を順番に処理する。
38+// テスト用の偽物のHTTPサーバー(MockServerが起動する)。使い方:php http-server.php ポート [証明書 秘密鍵]
39+// 1回に1つの接続を順番に処理する。証明書と秘密鍵を渡すとHTTPSにも対応する(TLSのClientHelloで判断、Std\Lib\Tls)。
4040
41+require_once __DIR__.'/../../../../autoload.php';
42+
43+use Std\Lib\Tls;
44+
4145 $port = (int)($argv[1] ?? 0);
46+$tls = isset($argv[2], $argv[3]) ? ['cert' => $argv[2], 'key' => $argv[3], 'alpn' => ['http/1.1']] : null;
4247 $server = @stream_socket_server("tcp://127.0.0.1:{$port}", $errno, $errstr);
4348 if (!$server) {
4449 fwrite(STDOUT, "error {$errstr}\n");
@@ -70,6 +75,16 @@
7075 $c = @stream_socket_accept($server, 60);
7176 if (!$c) break; // 60秒何も来なければ終了(テストが落ちても残らない様に)
7277 stream_set_timeout($c, 5);
78+
79+ // HTTPS(最初のバイトがTLSのハンドシェイク)
80+ if ($tls && stream_socket_recvfrom($c, 1, STREAM_PEEK) === "\x16") {
81+ try {
82+ $c = Tls::accept($c, $tls);
83+ } catch (\Throwable $e) {
84+ @fclose($c);
85+ continue;
86+ }
87+ }
7388
7489 // リクエストの読み込み
7590 $head = '';
@@ -52,6 +52,11 @@
5252 // @@lb_tls・@@lb_plugin・@@lb_full_auth・@@lb_caps・@@character_set_client・@@version、
5353 // DATABASE()・CONNECTION_ID()・VERSION()
5454
55+require_once __DIR__.'/../../../../autoload.php';
56+
57+use Std\Lib\Crypto\Rsa;
58+use Std\Lib\Tls;
59+
5560 $port = (int)($argv[1] ?? 0);
5661 $defaultPlugin = $argv[2] ?? 'mysql_native_password';
5762 $certFile = $argv[3] ?? '';
@@ -118,9 +123,9 @@
118123 public function __construct(private string $defaultPlugin, private string $certFile, private string $keyFile) {
119124 $this->pdo = new \PDO('sqlite::memory:');
120125 $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
121- if ($keyFile !== '' && function_exists('openssl_pkey_get_private')) {
122- $this->privKey = openssl_pkey_get_private((string)file_get_contents($keyFile)) ?: null;
123- if ($this->privKey) $this->pubPem = openssl_pkey_get_details($this->privKey)['key'];
126+ if ($keyFile !== '') {
127+ $this->privKey = Rsa::fromPem((string)file_get_contents($keyFile));
128+ $this->pubPem = $this->privKey->publicPem();
124129 }
125130 }
126131
@@ -129,7 +134,7 @@
129134 // TLSで既に復号されたデータが残っている接続を先に
130135 $pending = [];
131136 foreach ($this->conns as $id => $cn) {
132- if ($cn['tls'] && (stream_get_meta_data($cn['sock'])['unread_bytes'] ?? 0) > 0) $pending[] = $id;
137+ if ($cn['tls'] && Tls::pending($cn['sock'])) $pending[] = $id;
133138 }
134139
135140 if (empty($pending)) {
@@ -220,11 +225,11 @@
220225 // SSLリクエスト(32バイト)
221226 if (($caps & self::CLIENT_SSL) && strlen($pkt) === 32) {
222227 if ($this->certFile === '') return false;
223- $s = $cn['sock'];
224- stream_context_set_option($s, 'ssl', 'local_cert', $this->certFile);
225- stream_context_set_option($s, 'ssl', 'local_pk', $this->keyFile);
226- stream_context_set_option($s, 'ssl', 'verify_peer', false);
227- if (!@stream_socket_enable_crypto($s, true, STREAM_CRYPTO_METHOD_TLS_SERVER)) return false;
228+ try {
229+ $cn['sock'] = Tls::accept($cn['sock'], ['cert' => $this->certFile, 'key' => $this->keyFile]);
230+ } catch (\Throwable $e) {
231+ return false;
232+ }
228233 $cn['tls'] = true;
229234 $pkt = $this->recv($id);
230235 if ($pkt === null) return false;
@@ -357,7 +362,8 @@
357362 }
358363
359364 private function decrypt(string $data, string $scramble): ?string {
360- if (!$this->privKey || !openssl_private_decrypt($data, $out, $this->privKey, OPENSSL_PKCS1_OAEP_PADDING)) return null;
365+ $out = $this->privKey?->decrypt($data, 'sha1');
366+ if ($out === null) return null;
361367 $res = '';
362368 for ($i = 0; $i < strlen($out); $i++) $res .= $out[$i] ^ $scramble[$i % strlen($scramble)];
363369 return $res;
@@ -0,0 +1,17 @@
1+-----BEGIN CERTIFICATE-----
2+MIICpjCCAiygAwIBAgIUf8MFDLf49k2K3NPjzv+Jylx3oQIwCgYIKoZIzj0EAwIw
3+LDEQMA4GA1UECgwHTEIgVGVzdDEYMBYGA1UEAwwPTEIgSW50ZXJtZWRpYXRlMB4X
4+DTI2MTExMjAzMzkyN1oXDTI3MDIxMDAzMzkyN1owGjEYMBYGA1UEAwwPd3d3LmV4
5+YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvntRlSep
6+btSz77LftcdBwZfV5OOK69Euy3boBK46xPXR7qgsD9/Ql7rv0BsDyhJS69dj2AhT
7+98j1jD8lw7MjhUuIDS0ZOgoMc6tMklfRZah4MQKinPAEB/D174irSWwXlIj8iBiG
8+8DNNQQB1NbUcOWR3IYamST/Qmk/thuS+/RlIpvXzEILHQf3e5WUYwqeF3YB5yL6f
9+Nyut9zuKiDX4KOEEjueAd9axaP0z0fFS51xmbp6+Ak3PDvhafRejkO5dJFTGOVu0
10+h56coyPznR1HkI98gE/bZYqWexMxIdViE5NL9LilWIcsjOYWCMzPQwB2Prj+8vz9
11+I/y9VWTGzt1vAQIDAQABo3MwcTATBgNVHSUEDDAKBggrBgEFBQcDAjAaBgNVHREE
12+EzARgg93d3cuZXhhbXBsZS5jb20wHQYDVR0OBBYEFKoKWsSfW4/zhZIG2QbDVmZB
13+pYiyMB8GA1UdIwQYMBaAFHmkvTzOlj4r7/gbBo64xC5QCkuvMAoGCCqGSM49BAMC
14+A2gAMGUCMQDrw4FiFqNcjhqvEj9iMmc4UfqT+np5XNL9Mn2hrKhgneIF/HitIT4K
15+et/gEGgVixACMH7NMilX6OrU0JgrMbm1vm4ER8UeEzkqhRAX1FstCc7QvAAbZzkN
16+lyWOWgfWElMdMg==
17+-----END CERTIFICATE-----
@@ -0,0 +1,17 @@
1+-----BEGIN CERTIFICATE-----
2+MIICoDCCAiagAwIBAgIUf8MFDLf49k2K3NPjzv+Jylx3oQMwCgYIKoZIzj0EAwIw
3+LDEQMA4GA1UECgwHTEIgVGVzdDEYMBYGA1UEAwwPTEIgSW50ZXJtZWRpYXRlMB4X
4+DTI2MTExMjAzMzkyN1oXDTI3MDIxMDAzMzkyN1owGjEYMBYGA1UEAwwPd3d3LmV4
5+YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvntRlSep
6+btSz77LftcdBwZfV5OOK69Euy3boBK46xPXR7qgsD9/Ql7rv0BsDyhJS69dj2AhT
7+98j1jD8lw7MjhUuIDS0ZOgoMc6tMklfRZah4MQKinPAEB/D174irSWwXlIj8iBiG
8+8DNNQQB1NbUcOWR3IYamST/Qmk/thuS+/RlIpvXzEILHQf3e5WUYwqeF3YB5yL6f
9+Nyut9zuKiDX4KOEEjueAd9axaP0z0fFS51xmbp6+Ak3PDvhafRejkO5dJFTGOVu0
10+h56coyPznR1HkI98gE/bZYqWexMxIdViE5NL9LilWIcsjOYWCMzPQwB2Prj+8vz9
11+I/y9VWTGzt1vAQIDAQABo20wazAaBgNVHREEEzARgg93d3cuZXhhbXBsZS5jb20w
12+DQYEKgMEBQEB/wQCBQAwHQYDVR0OBBYEFKoKWsSfW4/zhZIG2QbDVmZBpYiyMB8G
13+A1UdIwQYMBaAFHmkvTzOlj4r7/gbBo64xC5QCkuvMAoGCCqGSM49BAMCA2gAMGUC
14+MHe6gmKztOoMU6sLbIUWWzOYw8HifadvOyQOObuT5lcNBmZpyAtFrcDY1Ta5M/km
15+ewIxALvG5o/Djq5qPDDEjN4zUYA+uKbkztZBofanWQAEy/Y1j5mWOLLiJdnmpho1
16+uXoQpw==
17+-----END CERTIFICATE-----
@@ -0,0 +1,19 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDGDCCAgCgAwIBAgIUEfAzqaxSXiaVYXsTCZHzNjLk+MUwDQYJKoZIhvcNAQEL
3+BQAwEjEQMA4GA1UEAwwHTEIgSW50MjAeFw0yNjExMTIwMzM5MjhaFw0yNzAyMTAw
4+MzM5MjhaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN
5+AQEBBQADggEPADCCAQoCggEBAL57UZUnqW7Us++y37XHQcGX1eTjiuvRLst26ASu
6+OsT10e6oLA/f0Je679AbA8oSUuvXY9gIU/fI9Yw/JcOzI4VLiA0tGToKDHOrTJJX
7+0WWoeDECopzwBAfw9e+Iq0lsF5SI/IgYhvAzTUEAdTW1HDlkdyGGpkk/0JpP7Ybk
8+vv0ZSKb18xCCx0H93uVlGMKnhd2Aeci+nzcrrfc7iog1+CjhBI7ngHfWsWj9M9Hx
9+UudcZm6evgJNzw74Wn0Xo5DuXSRUxjlbtIeenKMj850dR5CPfIBP22WKlnsTMSHV
10+YhOTS/S4pViHLIzmFgjMz0MAdj64/vL8/SP8vVVkxs7dbwECAwEAAaNeMFwwGgYD
11+VR0RBBMwEYIPd3d3LmV4YW1wbGUuY29tMB0GA1UdDgQWBBSqClrEn1uP84WSBtkG
12+w1ZmQaWIsjAfBgNVHSMEGDAWgBQBgzZbt3CafOnlK5ZTioe5gQLfLjANBgkqhkiG
13+9w0BAQsFAAOCAQEAkCsoR5jX7/G55/pVvvMA4W9LakJdhnFgUzObvEsC8iNwvnfn
14+lnaHapI6VH96t1MWFcunhBWi0R8G7kHyMZtq/GP2RNCiRtOAVpazGg6fhH0JgAuJ
15+INOtOsn0ejuX8HSijfcTdWEBFiC0ucKhuMIFJIk2tCODwBcDIfMS7MynkxCG2Uad
16+7c5tMBsDVSztc7ocjOZv+5Nfu3T55/iutCvg55L+dAqGLJ9mhe+W6SLIHCngVW0x
17+QYRvpSyI7CsvJxLKdFf4E2FGWKJ3fF1aP9Bpbf4w+5LYPGzEkzEYBJvYaFNHV8Hp
18+D26kPThJtap+n/8v26AOXKvdDzpGLWau8sKY0Q==
19+-----END CERTIFICATE-----
@@ -0,0 +1,12 @@
1+-----BEGIN CERTIFICATE-----
2+MIIB2zCCAWGgAwIBAgIUf8MFDLf49k2K3NPjzv+Jylx3oQUwCgYIKoZIzj0EAwIw
3+LDEQMA4GA1UECgwHTEIgVGVzdDEYMBYGA1UEAwwPTEIgSW50ZXJtZWRpYXRlMB4X
4+DTI2MTExMjA0NDAzNVoXDTI3MDIxMDA0NDAzNVowGjEYMBYGA1UEAwwPd3d3LmV4
5+YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbsvpG5wwC7asTRYS
6+UIeA7kV01aKFa4k5HH1THQ06+cnkHNe0zX5lvLy7h8Jn/JBhLFavwV5z3iN+Yc8A
7+fpe+dqNzMHEwGgYDVR0RBBMwEYIPd3d3LmV4YW1wbGUuY29tMBMGA1UdJQQMMAoG
8+CCsGAQUFBwMBMB0GA1UdDgQWBBRTX3woC8WiUxnnzsizNNNKSZgxQzAfBgNVHSME
9+GDAWgBR5pL08zpY+K+/4GwaOuMQuUApLrzAKBggqhkjOPQQDAgNoADBlAjAYmPSo
10+Q2Rpncj0x9qclx/D0eOO6ZRmPgAvW6sBvif9SRlmBstRr9BHCc3UDxwi6vQCMQDD
11+RYeLu+lGUdwNy41WZviyr5SRLKTJMJpusZpPZ4Q6p6NdwxtRtDZ5BaMvlVrpGRw=
12+-----END CERTIFICATE-----
@@ -0,0 +1,17 @@
1+-----BEGIN CERTIFICATE-----
2+MIICozCCAYugAwIBAgIUE9OcFNcWr56QYVNKyuBt1zBjxucwDQYJKoZIhvcNAQEL
3+BQAwMTELMAkGA1UEBhMCSlAxEDAOBgNVBAoMB0xCIFRlc3QxEDAOBgNVBAMMB0xC
4+IFJvb3QwHhcNMjYxMTEyMDMzOTI3WhcNMjkwODA4MDMzOTI3WjAsMRAwDgYDVQQK
5+DAdMQiBUZXN0MRgwFgYDVQQDDA9MQiBJbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIB
6+BgUrgQQAIgNiAAQF8GIMC0yKKKSlGr7wOtWfyVzXSF3chnpU+J06cQI2JuS3aenI
7+3GmZHItoE09PaSpqGFrH00qiF29LN+AUkRG2se6K+1Dj7j/6pdsoohfSFK4+FfyD
8+Vm0qD1LbEXvkPQejZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQD
9+AgEGMB0GA1UdDgQWBBR5pL08zpY+K+/4GwaOuMQuUApLrzAfBgNVHSMEGDAWgBSL
10+gGyHpGE0jd2M9Dlo6Zj0hsLU2DANBgkqhkiG9w0BAQsFAAOCAQEAIJRZxvLF3aO8
11+WRaxIJ9n6cO/6ExeL0bztECkbe5FJSpvliO3YV4qS9EJRWaiq0dtvhwMpu68/Q4E
12+NmISM8kJiYqNgrr/WxzKTMcO5YtUM25hff9pi/rO+PAb+wlHuZvJziSSUb+y7Wu5
13+vAVRJnlWWqUGQmLGYVpJDlvDENrWRUlmtrfTZs7GYhJMoYHYf76Ud8Jzq2c3CQWz
14+gRAdFsFy2yXEJuxnq2gWSW+k3Zw5+OCqy7VzO0ILsIUuFtyCSYFi7hrxeJWjG5NJ
15+qxKyStfKsr2Q8J+gXmvlu+P/Vd3G6aq+fhFmxtmhhbQcxL2S0wu+itIufha6gXE8
16+pDhI8J2qMA==
17+-----END CERTIFICATE-----
@@ -0,0 +1,16 @@
1+-----BEGIN CERTIFICATE-----
2+MIICjjCCAhSgAwIBAgIUf8MFDLf49k2K3NPjzv+Jylx3oQQwCgYIKoZIzj0EAwIw
3+LDEQMA4GA1UECgwHTEIgVGVzdDEYMBYGA1UEAwwPTEIgSW50ZXJtZWRpYXRlMB4X
4+DTI2MTExMjAzMzkyOFoXDTI5MDgwODAzMzkyOFowEjEQMA4GA1UEAwwHTEIgSW50
5+MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKRPnZuAiOIRtEGrnrO8
6+IekLUcRNrwucBDeZ3TcZ2bzyQKcKlXMwDCWTgSMEtalDrn2g1w/vKqoszfcqiPuT
7+iG1RK11736PG+gYMIbgpIW2OqPDWNZhUQm45n+owErQlvTv3VLv3QzmxG72sUaPB
8+yS5+dNVFaSLcmfIeDBnriZUy0MpV8MfiYHnWETtZYnS8YZO6g4JBxX7fAiGgSaAO
9+YSSeHIac+niXsU4WaF4bewOYium6C/odPwL0diDoPPyrXaa3PJsjdreOxX8vePRk
10+GBnntQqIsZnPzR+PfFs/LtTNfAjOg3uETE1ClgJQUNF6oiY3LApzRikofYvUfx72
11+02ECAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAgQwHQYD
12+VR0OBBYEFAGDNlu3cJp86eUrllOKh7mBAt8uMB8GA1UdIwQYMBaAFHmkvTzOlj4r
13+7/gbBo64xC5QCkuvMAoGCCqGSM49BAMCA2gAMGUCMQCWUxlqicLb4OweeO0bIYaF
14+PgDe14v5gt66jETrZ6gdYEcyUHrOLa686JNKrL8nGK4CMHY37nxku1qUf+sDp76o
15+p7/MHH5iGmnczcxgLBSEO1gluFK7YXfkPNOWOb/ZmNAmlA==
16+-----END CERTIFICATE-----
@@ -0,0 +1,18 @@
1+-----BEGIN CERTIFICATE-----
2+MIIC3TCCAmKgAwIBAgIUf8MFDLf49k2K3NPjzv+Jylx3oQEwCgYIKoZIzj0EAwIw
3+LDEQMA4GA1UECgwHTEIgVGVzdDEYMBYGA1UEAwwPTEIgSW50ZXJtZWRpYXRlMB4X
4+DTI2MTExMjAzMzkyN1oXDTI3MDIxMDAzMzkyN1owGjEYMBYGA1UEAwwPd3d3LmV4
5+YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvntRlSep
6+btSz77LftcdBwZfV5OOK69Euy3boBK46xPXR7qgsD9/Ql7rv0BsDyhJS69dj2AhT
7+98j1jD8lw7MjhUuIDS0ZOgoMc6tMklfRZah4MQKinPAEB/D174irSWwXlIj8iBiG
8+8DNNQQB1NbUcOWR3IYamST/Qmk/thuS+/RlIpvXzEILHQf3e5WUYwqeF3YB5yL6f
9+Nyut9zuKiDX4KOEEjueAd9axaP0z0fFS51xmbp6+Ak3PDvhafRejkO5dJFTGOVu0
10+h56coyPznR1HkI98gE/bZYqWexMxIdViE5NL9LilWIcsjOYWCMzPQwB2Prj+8vz9
11+I/y9VWTGzt1vAQIDAQABo4GoMIGlMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgWg
12+MBMGA1UdJQQMMAoGCCsGAQUFBwMBMDMGA1UdEQQsMCqCD3d3dy5leGFtcGxlLmNv
13+bYIRKi5hcGkuZXhhbXBsZS5jb22HBMAAAgcwHwYDVR0jBBgwFoAUeaS9PM6WPivv
14++BsGjrjELlAKS68wHQYDVR0OBBYEFKoKWsSfW4/zhZIG2QbDVmZBpYiyMAoGCCqG
15+SM49BAMCA2kAMGYCMQCTca2qdBPkmi6On3/ExAnrQZqSuAj/DUbY+NKUBlLgaa92
16+YjXk1Nf3Hxuux//hps4CMQDx6tMUIvV6qCdRhBDNIgrR5alLG8GYFaKALn5SE84n
17+lC8dGBWC6EXcVzy7BzMF9+A=
18+-----END CERTIFICATE-----
@@ -0,0 +1,21 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDYTCCAkmgAwIBAgIUE9OcFNcWr56QYVNKyuBt1zBjxukwDQYJKoZIhvcNAQEL
3+BQAwMTELMAkGA1UEBhMCSlAxEDAOBgNVBAoMB0xCIFRlc3QxEDAOBgNVBAMMB0xC
4+IFJvb3QwHhcNMjYxMTEyMDMzOTI4WhcNMjkwODA4MDMzOTI4WjASMRAwDgYDVQQD
5+DAdMQiBJbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApE+dm4CI
6+4hG0Qaues7wh6QtRxE2vC5wEN5ndNxnZvPJApwqVczAMJZOBIwS1qUOufaDXD+8q
7+qizN9yqI+5OIbVErXXvfo8b6BgwhuCkhbY6o8NY1mFRCbjmf6jAStCW9O/dUu/dD
8+ObEbvaxRo8HJLn501UVpItyZ8h4MGeuJlTLQylXwx+JgedYRO1lidLxhk7qDgkHF
9+ft8CIaBJoA5hJJ4chpz6eJexThZoXht7A5iK6boL+h0/AvR2IOg8/Ktdprc8myN2
10+t47Ffy949GQYGee1Coixmc/NH498Wz8u1M18CM6De4RMTUKWAlBQ0XqiJjcsCnNG
11+KSh9i9R/HvbTYQIDAQABo4GPMIGMMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
12+BAQDAgIEMCkGA1UdHgEB/wQfMB2gGzANggtleGFtcGxlLm9yZzAKhwgKAAAA/wAA
13+ADAdBgNVHQ4EFgQUAYM2W7dwmnzp5SuWU4qHuYEC3y4wHwYDVR0jBBgwFoAUi4Bs
14+h6RhNI3djPQ5aOmY9IbC1NgwDQYJKoZIhvcNAQELBQADggEBAGHgcsC88OKdTG8o
15+m1YjwtAO0I7LH8MBgYAPftUgPK6PRdPsl7Uknwy1gTZKqt9BjWDxK5pnExnL463Q
16+hXSM1wRYQ2YcW2t1+JqNOa0/jOoQuEFCS1NWnUI7THuBZeY+me+AbO+x3Zl9PTHv
17+0r633p1w831kpN4NIxgfOSJviiZa2LLuhrF8N1UvrbuGVTcKsu6Hr6tzq9luVg1P
18+NOHJw4dB1evzU21GjdTLoR2fHagmVcTChVUewdV85BSS81m7MSbsBhsmmQbQjM2w
19+EmilQ4rdScfcySRO4WA/qXadqeJJ6EEpiS7DBnL0mvZBCZcR56ClsGFuJa3XK9C9
20+mCmRKEs=
21+-----END CERTIFICATE-----
@@ -0,0 +1,19 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDGDCCAgCgAwIBAgIUVd/dx/7Kqo9AQAtSTR7/CyFq3W4wDQYJKoZIhvcNAQEL
3+BQAwEjEQMA4GA1UEAwwHTEIgSW50MjAeFw0yNjExMTIwMzM5MjhaFw0yNzAyMTAw
4+MzM5MjhaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN
5+AQEBBQADggEPADCCAQoCggEBAL57UZUnqW7Us++y37XHQcGX1eTjiuvRLst26ASu
6+OsT10e6oLA/f0Je679AbA8oSUuvXY9gIU/fI9Yw/JcOzI4VLiA0tGToKDHOrTJJX
7+0WWoeDECopzwBAfw9e+Iq0lsF5SI/IgYhvAzTUEAdTW1HDlkdyGGpkk/0JpP7Ybk
8+vv0ZSKb18xCCx0H93uVlGMKnhd2Aeci+nzcrrfc7iog1+CjhBI7ngHfWsWj9M9Hx
9+UudcZm6evgJNzw74Wn0Xo5DuXSRUxjlbtIeenKMj850dR5CPfIBP22WKlnsTMSHV
10+YhOTS/S4pViHLIzmFgjMz0MAdj64/vL8/SP8vVVkxs7dbwECAwEAAaNeMFwwGgYD
11+VR0RBBMwEYIPd3d3LmV4YW1wbGUuY29tMB0GA1UdDgQWBBSqClrEn1uP84WSBtkG
12+w1ZmQaWIsjAfBgNVHSMEGDAWgBQBgzZbt3CafOnlK5ZTioe5gQLfLjANBgkqhkiG
13+9w0BAQsFAAOCAQEAoOyc5y0m3kcqIo4SmOQ4Dfc/yJapciOCEXsaNXl0JiJYvdgG
14+WstIflFDpmd1KFr0///yDefFzpitMVZ+KapK4FQ4NYuOhoegLI5zdkSlGmMe9Brv
15+KpxuLnVRQFJo2x8bhEVZr5HOT4Q08nUADGu1oytarSRmJAgxGWF06/WjW5QtI7Bg
16+ANLYxmWStgoJMRkgkJPvF49YT6e5vAGXdUqmHRceyl0pQzz9NME75ohN71zLFktB
17+KLiCRL5H8UYNKWL3mTkwUd5OaKCapCkTyyqbbsf4Ok0GYNYo7AxPBjAhMFIGO5Gq
18+SiEgvam5C13P4SrIbONXqUJxIFsKVObtZbbPZQ==
19+-----END CERTIFICATE-----
@@ -0,0 +1,19 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDHDCCAgSgAwIBAgIUVd/dx/7Kqo9AQAtSTR7/CyFq3W8wDQYJKoZIhvcNAQEL
3+BQAwEjEQMA4GA1UEAwwHTEIgSW50MjAeFw0yNjExMTIwMzM5MjhaFw0yNzAyMTAw
4+MzM5MjhaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN
5+AQEBBQADggEPADCCAQoCggEBAL57UZUnqW7Us++y37XHQcGX1eTjiuvRLst26ASu
6+OsT10e6oLA/f0Je679AbA8oSUuvXY9gIU/fI9Yw/JcOzI4VLiA0tGToKDHOrTJJX
7+0WWoeDECopzwBAfw9e+Iq0lsF5SI/IgYhvAzTUEAdTW1HDlkdyGGpkk/0JpP7Ybk
8+vv0ZSKb18xCCx0H93uVlGMKnhd2Aeci+nzcrrfc7iog1+CjhBI7ngHfWsWj9M9Hx
9+UudcZm6evgJNzw74Wn0Xo5DuXSRUxjlbtIeenKMj850dR5CPfIBP22WKlnsTMSHV
10+YhOTS/S4pViHLIzmFgjMz0MAdj64/vL8/SP8vVVkxs7dbwECAwEAAaNiMGAwHgYD
11+VR0RBBcwFYINYS5leGFtcGxlLm9yZ4cECgECAzAdBgNVHQ4EFgQUqgpaxJ9bj/OF
12+kgbZBsNWZkGliLIwHwYDVR0jBBgwFoAUAYM2W7dwmnzp5SuWU4qHuYEC3y4wDQYJ
13+KoZIhvcNAQELBQADggEBAJkygGXtVsjuXzawfyv4FdYpRcvjekqLNTn+Dbui85rB
14+UAluWssjGOTtFKJMkWNZS+u4yyb33Q3gItmjm9PWtdw+gYdP0LIidw441JDXwTbI
15+GxRftN194qnrnx2d/xOB3cYEOSSuuQjXBc04D8NfVtjD4vzldR3QXpq5HALOnPLS
16+TW00y8kGN4MDhejd+7ObjFibUj99xZlBOe4P2c4+YBhpM1y/8vJkZb0WUsEXI3Qi
17+dpJMAQZnE23/mUlR2GxKVsn4Pea7rKLZ1J6VmOFc+bGPQaoC8Gw8melVpbTAFv1a
18+hEBDns/LhmY0lvkludwzILrWZdxq3dCAz/t/zrNH6kU=
19+-----END CERTIFICATE-----
@@ -0,0 +1,16 @@
1+-----BEGIN CERTIFICATE-----
2+MIICijCCAXKgAwIBAgIUE9OcFNcWr56QYVNKyuBt1zBjxugwDQYJKoZIhvcNAQEL
3+BQAwMTELMAkGA1UEBhMCSlAxEDAOBgNVBAoMB0xCIFRlc3QxEDAOBgNVBAMMB0xC
4+IFJvb3QwHhcNMjYxMTEyMDMzOTI3WhcNMjkwODA4MDMzOTI3WjAsMRAwDgYDVQQK
5+DAdMQiBUZXN0MRgwFgYDVQQDDA9MQiBJbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIB
6+BgUrgQQAIgNiAAQF8GIMC0yKKKSlGr7wOtWfyVzXSF3chnpU+J06cQI2JuS3aenI
7+3GmZHItoE09PaSpqGFrH00qiF29LN+AUkRG2se6K+1Dj7j/6pdsoohfSFK4+FfyD
8+Vm0qD1LbEXvkPQejTTBLMAkGA1UdEwQCMAAwHQYDVR0OBBYEFHmkvTzOlj4r7/gb
9+Bo64xC5QCkuvMB8GA1UdIwQYMBaAFIuAbIekYTSN3Yz0OWjpmPSGwtTYMA0GCSqG
10+SIb3DQEBCwUAA4IBAQBt/tKRls10tdhGjdzlLjOeL7cGVSVzt44m6aqDQ3y5TGO9
11+67LuDnypbSfIzUHRQSaDRmkhBdlzgxZu9LVY78JeZXczIESsj2DBQ8EAGoKAv5zV
12+33zABSJOFhUs6XByOsg2zNugAoN4nxxJT/JkYmLjYG9oQVSlerrtL6b4lWhmNvvB
13+PslCC3XUhlzm5h/4KWKXr7KQ1f9Sey08vk08bhfMVQGue0cAOxa/H5gQ1CWYQ29W
14+kOzpEAjeHx41YCp7M8H2CLSg0vRqZAYabDCe/aBf3MtW4TI4w0cODrVtPVJxdsqi
15+ilHxLPYIkKFupQiTS07aqQyQcKVf8qJ4no9MLGxC
16+-----END CERTIFICATE-----
@@ -0,0 +1,5 @@
1+-----BEGIN EC PRIVATE KEY-----
2+MHcCAQEEIAO4F/kPUVbv8urTuZ8loenr/ctpvBahYnBbtWYSBYlsoAoGCCqGSM49
3+AwEHoUQDQgAEbsvpG5wwC7asTRYSUIeA7kV01aKFa4k5HH1THQ06+cnkHNe0zX5l
4+vLy7h8Jn/JBhLFavwV5z3iN+Yc8Afpe+dg==
5+-----END EC PRIVATE KEY-----
@@ -0,0 +1,5 @@
1+-----BEGIN PRIVATE KEY-----
2+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgA7gX+Q9RVu/y6tO5
3+nyWh6ev9y2m8FqFicFu1ZhIFiWyhRANCAARuy+kbnDALtqxNFhJQh4DuRXTVooVr
4+iTkcfVMdDTr5yeQc17TNfmW8vLuHwmf8kGEsVq/BXnPeI35hzwB+l752
5+-----END PRIVATE KEY-----
@@ -0,0 +1,4 @@
1+-----BEGIN PUBLIC KEY-----
2+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbsvpG5wwC7asTRYSUIeA7kV01aKF
3+a4k5HH1THQ06+cnkHNe0zX5lvLy7h8Jn/JBhLFavwV5z3iN+Yc8Afpe+dg==
4+-----END PUBLIC KEY-----
@@ -0,0 +1,6 @@
1+-----BEGIN PRIVATE KEY-----
2+MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDD/O80/Uby+cOL19qX5
3+ZHi+MaEYWd1qylZQaK0kOs1Nsa/e+MCcf94NpLz81NbEQaWhZANiAAR3qMZjzYl3
4+5D0FFDosSaACnj0bYEuTQtO9h0romUfrCh/1i1wnJzYZLw8okyYAaLan8fisVB24
5+PwCr++4dliZfN35SMJydV+pDUs9zAUYJLYLo+vif3gCDQw9eLfBxKxc=
6+-----END PRIVATE KEY-----
@@ -0,0 +1,5 @@
1+-----BEGIN PUBLIC KEY-----
2+MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEd6jGY82Jd+Q9BRQ6LEmgAp49G2BLk0LT
3+vYdK6JlH6wof9YtcJyc2GS8PKJMmAGi2p/H4rFQduD8Aq/vuHZYmXzd+UjCcnVfq
4+Q1LPcwFGCS2C6Pr4n94Ag0MPXi3wcSsX
5+-----END PUBLIC KEY-----
@@ -0,0 +1,22 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDnzCCAlOgAwIBAgIUE9OcFNcWr56QYVNKyuBt1zBjxuowQQYJKoZIhvcNAQEK
3+MDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEF
4+AKIDAgEgMDExCzAJBgNVBAYTAkpQMRAwDgYDVQQKDAdMQiBUZXN0MRAwDgYDVQQD
5+DAdMQiBSb290MB4XDTI2MTExMjAzMzkyOFoXDTI3MDIxMDAzMzkyOFowGjEYMBYG
6+A1UEAwwPd3d3LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
7+CgKCAQEAvntRlSepbtSz77LftcdBwZfV5OOK69Euy3boBK46xPXR7qgsD9/Ql7rv
8+0BsDyhJS69dj2AhT98j1jD8lw7MjhUuIDS0ZOgoMc6tMklfRZah4MQKinPAEB/D1
9+74irSWwXlIj8iBiG8DNNQQB1NbUcOWR3IYamST/Qmk/thuS+/RlIpvXzEILHQf3e
10+5WUYwqeF3YB5yL6fNyut9zuKiDX4KOEEjueAd9axaP0z0fFS51xmbp6+Ak3PDvha
11+fRejkO5dJFTGOVu0h56coyPznR1HkI98gE/bZYqWexMxIdViE5NL9LilWIcsjOYW
12+CMzPQwB2Prj+8vz9I/y9VWTGzt1vAQIDAQABo14wXDAaBgNVHREEEzARgg9wc3Mu
13+ZXhhbXBsZS5jb20wHQYDVR0OBBYEFKoKWsSfW4/zhZIG2QbDVmZBpYiyMB8GA1Ud
14+IwQYMBaAFIuAbIekYTSN3Yz0OWjpmPSGwtTYMEEGCSqGSIb3DQEBCjA0oA8wDQYJ
15+YIZIAWUDBAIBBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAIBBQCiAwIBIAOC
16+AQEAjIMS2Wp+KiHpI1ntfew2keyMOFZS79xypS3EOFVMr1Q2KknXpIjSQX8RSGEC
17+ED1v3o3Iar3o/RBK01NuzDr0CoRWLD5R4YSpovhW/D6KQweyxRWjIXpWAuUy0SYN
18+Um5EeMR8LhXAXXb4HSDTV0sq0jByCaasBDVvkXAIBLCMPF0eLCfvWPjbCAbpIYWw
19+9DWZEtPAJLFgFeKR0aiTMCLUPuFtKXEmCFVbvSrL2REiPNM0H0jAjmv/lJNUgMj6
20+jHxgo4CcSzHjVLnE927Z6xEpehM0HlaS5cyz1oS5vX9PxvJk6gOotIA2kRFISpUP
21+lAX5a1LJ2udrUpziXkPZKLbasA==
22+-----END CERTIFICATE-----
@@ -0,0 +1,20 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDUzCCAjugAwIBAgIUQzgDe8N30G4N9UjrsMdN/JVm71IwDQYJKoZIhvcNAQEL
3+BQAwMTELMAkGA1UEBhMCSlAxEDAOBgNVBAoMB0xCIFRlc3QxEDAOBgNVBAMMB0xC
4+IFJvb3QwHhcNMjYxMTEyMDMzOTI3WhcNMzYxMTA5MDMzOTI3WjAxMQswCQYDVQQG
5+EwJKUDEQMA4GA1UECgwHTEIgVGVzdDEQMA4GA1UEAwwHTEIgUm9vdDCCASIwDQYJ
6+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKBsgqp3iQ7ZbcQrh30cAzeKhOAT2Kre
7+yV6Ox7mSSW2XBJeDdNaY/c8RFoxLxieQFTuUSP0imVKY8gdI4jO5Btl2N9DBNjT7
8+7OOqd8d4ZXhSsOS+MwxCk2uc6mvqIV3LdlGleTLi8ewHXqOqWDEqy5zTp2uq+YSM
9+Zi5QK8WUs5Fx/6k3cXhm+wAHLPYol9llhCCfFmE46rUvM400C8BqwKu5qDWspNSk
10+f7YWge5iz1EFZdPbJYIFAl7AfjQI4dBjsWwNYMc/I3bNbE/FtVLSeKqs3FEQ448q
11+wiy7VRMk/L+94i/msGD5h4a/DCT0asKfYmdxUUWf+VpV7YKsXH2xdfUCAwEAAaNj
12+MGEwHQYDVR0OBBYEFIuAbIekYTSN3Yz0OWjpmPSGwtTYMB8GA1UdIwQYMBaAFIuA
13+bIekYTSN3Yz0OWjpmPSGwtTYMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
14+AgEGMA0GCSqGSIb3DQEBCwUAA4IBAQB104/QP/wJj5Tv7aspm6R8F0M3bnImEq6d
15+PPL4WWdcCDp+vIxRg1Rcma2Wni038AJwqoWp/0PyL8SiiaN7b4RjXEFGW5A2Vyhg
16+ccQeBOPZ/ltCKf1iR/e91l2SsQBAD9Scl6jXUgwbP3wEaGJYdg6TcCg3vv2AVcoB
17+lmiU/IczzoYiGpi+/gQyiD+4SgHWdF3urScnvppct/yiuTJM+lxHpyYKCxXVSk3q
18+a3bZXD+DSF/nnrkH58+u+fNTiRzjHUaMGsPsb4WD8JJh1NIKNU7IbwrPPD10XFb2
19+vF/k6u5pYOzCvxtFLTuKi1pEjxbsYz2oCSGerxlGy0a+W6af42j/
20+-----END CERTIFICATE-----
@@ -0,0 +1,27 @@
1+-----BEGIN RSA PRIVATE KEY-----
2+MIIEpAIBAAKCAQEAtK66S5m6ZgBn5+pzMGvdTEAZG9E2EQVhT9tjSIBHssDelFw4
3+7/f4eg+q2d54C71CogD4QlQkq4oIaj3rm99nFME9NuBSKLqTHKqG3LmIA8rV56uX
4+IboHFXMIBM3qtBGPEWFxYunTER7w59d6oKzam7NXjV3aXfYK2h2zh+J3AcRrCJpG
5+2qC5ebBNsAcLLuBYJ2CKMCPeBQG4/5AFe3QDAwtISBUOcLyYKoxECIwaDhKDRqDu
6+ZlnuW1U1T0q9fyg1V7n9n2w35wd+QHVx3NNkkEZRC9fwim8CqpRbfivPghFPuMdx
7+St31e/MurPeLYQvidZVyiJjkIraxbu7k41eS4wIDAQABAoIBAEDZA0j19DX+yEwu
8+08fZttPSn5Bhzp9eSHZSXbhcPh90ZI5n6RGLdLVOYToPxBMmir89TNz1eTHN+UY0
9+UhcgH82Nq2xCJ4qxp/IebAfVWPFPf+VFqxjyPO+gWQzsYOPe93O7/ueK13OqILl9
10+F2iKkW1XeUFmIWUe1PdCuLg4+hB6NZnlB2GDn7wmo6VmDpsYH2gtklLDWTHUfd3G
11+rtDyg+HKW/BF7cNeB1+aVmHxPsQYx9ociTGPf7ab60T/4fvY9LzmOYL/TclNOBai
12+czSJ410U2xN0GT8yhFAB1PDrrbqnWVAk1WXjwn0TEEoI2jQ2OMN3U+afTdYtxYW7
13+tcvEeAECgYEA3yGPfJ5Vw1TeSBN9XAZrpmZhOu9xZCSvRluk0gOPJ90ipwdAh88O
14+HbEc9DHaIjMhaY3kizJNlmH4Pdi/HgOTtAX9zrSUL842qzqWZDtEuYe6GV98+Pj0
15+Wb7kQDPziG9GvvMez4tjlXG8EKu2LTEMU4QBXh7SbEL5eGQTTsWjqAECgYEAz0xm
16+dypfcRwgL2S4AUjTqOLJLf/fSj3yW/pE6JqJ2qvbUncJpg4O0ACNm1BUXzPngdBq
17+HVcNrIXJwtM8A772JDugFovizalSzblkM0LvCdzwoN8Q26jLZQzFzcHmk+tXtIZm
18+uxaP2ZXBDuW2c6TYlvZ/MgiajZp4C0mWsUgpmuMCgYEAx6qLcLNrAVjvc8Xk9rVU
19+HVoeBZQAfuGI6sfVLI08zCpI+Pg3WZACDLXRt/7oOJRSgNcVPS4tWbWfYxCSrPP8
20+dyqpXRAgTssUBpNtwpF6xR7cfBoDhfOZDLoSHBWhBmS1BOgfmQ8ddLyK0swlQDlU
21+02MbqjKgj3o7LO04jdOzyAECgYBVBtBMdovZOCEXb2dt13JBpmvAO2BZ8lizU07q
22+XWDH2Y/HHl5NxxxaQq/SkbtZpU+C1np/+FH6fpHIIJ5QJwaMzAwY012+34HAMheg
23+Uwb6862bPoOKpYxvsTJ899hcub4qpKoEePed1qfuNViFzIquOQLpAdBq3n34m0F9
24+C81M4wKBgQCjVBqJ0Xwf3/OXhUpAvhx13Vc+ZxRF8YieL0xsrUvO+1ATFhZzvSwe
25+SJp/vC/s5YvKu6tlnzGuiq4mjZi9wAgl0/W8pQjWJzubPzkoe4iV9kzWOGylxnQu
26+YROQeGLSuPsGRhf7uFgSQ7rUBLTOymNd7hY5X8tJyJXqp+muFHoV9A==
27+-----END RSA PRIVATE KEY-----
@@ -0,0 +1,8 @@
1+-----BEGIN RSA PUBLIC KEY-----
2+MIIBCgKCAQEAtK66S5m6ZgBn5+pzMGvdTEAZG9E2EQVhT9tjSIBHssDelFw47/f4
3+eg+q2d54C71CogD4QlQkq4oIaj3rm99nFME9NuBSKLqTHKqG3LmIA8rV56uXIboH
4+FXMIBM3qtBGPEWFxYunTER7w59d6oKzam7NXjV3aXfYK2h2zh+J3AcRrCJpG2qC5
5+ebBNsAcLLuBYJ2CKMCPeBQG4/5AFe3QDAwtISBUOcLyYKoxECIwaDhKDRqDuZlnu
6+W1U1T0q9fyg1V7n9n2w35wd+QHVx3NNkkEZRC9fwim8CqpRbfivPghFPuMdxSt31
7+e/MurPeLYQvidZVyiJjkIraxbu7k41eS4wIDAQAB
8+-----END RSA PUBLIC KEY-----
@@ -0,0 +1,28 @@
1+-----BEGIN PRIVATE KEY-----
2+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0rrpLmbpmAGfn
3+6nMwa91MQBkb0TYRBWFP22NIgEeywN6UXDjv9/h6D6rZ3ngLvUKiAPhCVCSrighq
4+Peub32cUwT024FIoupMcqobcuYgDytXnq5chugcVcwgEzeq0EY8RYXFi6dMRHvDn
5+13qgrNqbs1eNXdpd9graHbOH4ncBxGsImkbaoLl5sE2wBwsu4FgnYIowI94FAbj/
6+kAV7dAMDC0hIFQ5wvJgqjEQIjBoOEoNGoO5mWe5bVTVPSr1/KDVXuf2fbDfnB35A
7+dXHc02SQRlEL1/CKbwKqlFt+K8+CEU+4x3FK3fV78y6s94thC+J1lXKImOQitrFu
8+7uTjV5LjAgMBAAECggEAQNkDSPX0Nf7ITC7Tx9m209KfkGHOn15IdlJduFw+H3Rk
9+jmfpEYt0tU5hOg/EEyaKvz1M3PV5Mc35RjRSFyAfzY2rbEInirGn8h5sB9VY8U9/
10+5UWrGPI876BZDOxg4973c7v+54rXc6oguX0XaIqRbVd5QWYhZR7U90K4uDj6EHo1
11+meUHYYOfvCajpWYOmxgfaC2SUsNZMdR93cau0PKD4cpb8EXtw14HX5pWYfE+xBjH
12+2hyJMY9/tpvrRP/h+9j0vOY5gv9NyU04FqJzNInjXRTbE3QZPzKEUAHU8OutuqdZ
13+UCTVZePCfRMQSgjaNDY4w3dT5p9N1i3Fhbu1y8R4AQKBgQDfIY98nlXDVN5IE31c
14+BmumZmE673FkJK9GW6TSA48n3SKnB0CHzw4dsRz0MdoiMyFpjeSLMk2WYfg92L8e
15+A5O0Bf3OtJQvzjarOpZkO0S5h7oZX3z4+PRZvuRAM/OIb0a+8x7Pi2OVcbwQq7Yt
16+MQxThAFeHtJsQvl4ZBNOxaOoAQKBgQDPTGZ3Kl9xHCAvZLgBSNOo4skt/99KPfJb
17++kTomonaq9tSdwmmDg7QAI2bUFRfM+eB0GodVw2shcnC0zwDvvYkO6AWi+LNqVLN
18+uWQzQu8J3PCg3xDbqMtlDMXNweaT61e0hma7Fo/ZlcEO5bZzpNiW9n8yCJqNmngL
19+SZaxSCma4wKBgQDHqotws2sBWO9zxeT2tVQdWh4FlAB+4Yjqx9UsjTzMKkj4+DdZ
20+kAIMtdG3/ug4lFKA1xU9Li1ZtZ9jEJKs8/x3KqldECBOyxQGk23CkXrFHtx8GgOF
21+85kMuhIcFaEGZLUE6B+ZDx10vIrSzCVAOVTTYxuqMqCPejss7TiN07PIAQKBgFUG
22+0Ex2i9k4IRdvZ23XckGma8A7YFnyWLNTTupdYMfZj8ceXk3HHFpCr9KRu1mlT4LW
23+en/4Ufp+kcggnlAnBozMDBjTXb7fgcAyF6BTBvrzrZs+g4qljG+xMnz32Fy5viqk
24+qgR4953Wp+41WIXMiq45AukB0GreffibQX0LzUzjAoGBAKNUGonRfB/f85eFSkC+
25+HHXdVz5nFEXxiJ4vTGytS877UBMWFnO9LB5Imn+8L+zli8q7q2WfMa6KriaNmL3A
26+CCXT9bylCNYnO5s/OSh7iJX2TNY4bKXGdC5hE5B4YtK4+wZGF/u4WBJDutQEtM7K
27+Y13uFjlfy0nIleqn6a4UehX0
28+-----END PRIVATE KEY-----
@@ -0,0 +1,9 @@
1+-----BEGIN PUBLIC KEY-----
2+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtK66S5m6ZgBn5+pzMGvd
3+TEAZG9E2EQVhT9tjSIBHssDelFw47/f4eg+q2d54C71CogD4QlQkq4oIaj3rm99n
4+FME9NuBSKLqTHKqG3LmIA8rV56uXIboHFXMIBM3qtBGPEWFxYunTER7w59d6oKza
5+m7NXjV3aXfYK2h2zh+J3AcRrCJpG2qC5ebBNsAcLLuBYJ2CKMCPeBQG4/5AFe3QD
6+AwtISBUOcLyYKoxECIwaDhKDRqDuZlnuW1U1T0q9fyg1V7n9n2w35wd+QHVx3NNk
7+kEZRC9fwim8CqpRbfivPghFPuMdxSt31e/MurPeLYQvidZVyiJjkIraxbu7k41eS
8+4wIDAQAB
9+-----END PUBLIC KEY-----
@@ -0,0 +1,22 @@
1+{
2+ "random": "883799680114fde268afe9f12923e1a632552caeaf1d9d600fe916aafac58d2e",
3+ "session_id": "b35cc6ac6db15743ebf0d72666930842aacfafe72e8550ac7bb8e4c489f43a49",
4+ "x25519": "a7451e822716cbf0d8c64128285edd0d555afcf293828c576afeeebedbe39647",
5+ "time": 1794462079,
6+ "lines": [
7+ "hello",
8+ "二番目",
9+ "third line"
10+ ],
11+ "replies_hex": [
12+ "6f6c6c6568",
13+ "ae9be7aa95e78cbae4",
14+ "656e696c206472696874"
15+ ],
16+ "version": "TLSv1.2",
17+ "cipher": "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
18+ "group": "P-384",
19+ "note": "OpenSSL 3.0のs_server(-rev)との記録。証明書はroot.pem・int.pem・leaf.pem(又はec12.pem)",
20+ "c2s": "16030100dd010000d90303883799680114fde268afe9f12923e1a632552caeaf1d9d600fe916aafac58d2e20b35cc6ac6db15743ebf0d72666930842aacfafe72e8550ac7bb8e4c489f43a490012130313011302cca9cca8c02bc02fc02cc0300100007e00000014001200000f7777772e6578616d706c652e636f6d000a00080006001d00170018000d00140012040305030603080408050806040105010601002b00050403040303003300260024001d0020e8a1850048a8385dd4264728a7f1c315d26890a3f8b087559bd2eb172398076e000b0002010000170000ff01000100160303006610000062610477a8c663cd8977e43d05143a2c49a0029e3d1b604b9342d3bd874ae89947eb0a1ff58b5c272736192f0f2893260068b6a7f1f8ac541db83f00abfbee1d96265f377e52309c9d57ea4352cf730146092d82e8faf89fde0083430f5e2df0712b1714030300010116030300202ca20b333c3193cc691e7b7116feab44781227974533ca0e014f633583d7b0151703030016f3171eb76ada0923377128aafc19e172c6c35f8ae560170303001a1ae56f540825468d6ca2ea25d6741e98b3ef7dbcb796d693c30f170303001b871c21296b7716fef35afa79a958e3ca39ffc4da8b4620d6c1067c1503030012a33dfa00bf9ebbb978b94ca05756460ac4c9",
21+ "s2c": "160303005d020000590303812c3de8d2ff2573d0bd809adf25e49f6a1900399f8496d3e03022f6c260dd7f20beb65c1e76f12277887bbf4a7e0db799b1e8aced9b7b9a16926f2b954dd9b556cca9000011ff01000100000b0004030001020017000016030304930b00048f00048c0001df308201db30820161a00302010202147fc3050cb7f8f64d8adcd3e3ceff89ca5c77a105300a06082a8648ce3d040302302c3110300e060355040a0c074c4220546573743118301606035504030c0f4c4220496e7465726d656469617465301e170d3236313131323034343033355a170d3237303231303034343033355a301a3118301606035504030c0f7777772e6578616d706c652e636f6d3059301306072a8648ce3d020106082a8648ce3d030107034200046ecbe91b9c300bb6ac4d1612508780ee4574d5a2856b89391c7d531d0d3af9c9e41cd7b4cd7e65bcbcbb87c267fc90612c56afc15e73de237e61cf007e97be76a3733071301a0603551d1104133011820f7777772e6578616d706c652e636f6d30130603551d25040c300a06082b06010505070301301d0603551d0e04160414535f7c280bc5a25319e7cec8b334d34a49983143301f0603551d2304183016801479a4bd3cce963e2beff81b068eb8c42e500a4baf300a06082a8648ce3d040302036800306502301898f4a84364699dc8f4c7da9c971fc3d1e38ee994663e002f5bab01be27fd49196606cb51afd04709cdd40f1c22eaf4023100c345878bbbe94651dc0dcb8d5666f8b2af94912ca4c9309a6eb19a4f67843aa7a35dc31b51b4367905a32f955ae9191c0002a7308202a33082018ba003020102021413d39c14d716af9e9061534acae06dd73063c6e7300d06092a864886f70d01010b05003031310b3009060355040613024a503110300e060355040a0c074c4220546573743110300e06035504030c074c4220526f6f74301e170d3236313131323033333932375a170d3239303830383033333932375a302c3110300e060355040a0c074c4220546573743118301606035504030c0f4c4220496e7465726d6564696174653076301006072a8648ce3d020106052b810400220362000405f0620c0b4c8a28a4a51abef03ad59fc95cd7485ddc867a54f89d3a71023626e4b769e9c8dc69991c8b68134f4f692a6a185ac7d34aa2176f4b37e0149111b6b1ee8afb50e3ee3ffaa5db28a217d214ae3e15fc83566d2a0f52db117be43d07a366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020106301d0603551d0e0416041479a4bd3cce963e2beff81b068eb8c42e500a4baf301f0603551d230418301680148b806c87a461348ddd8cf43968e998f486c2d4d8300d06092a864886f70d01010b05000382010100209459c6f2c5dda3bc5916b1209f67e9c3bfe84c5e2f46f3b440a46dee45252a6f9623b7615e2a4bd1094566a2ab476dbe1c0ca6eebcfd0e0436621233c909898a8d82baff5b1cca4cc70ee58b54336e617dff698bfacef8f01bfb0947b99bc9ce249251bfb2ed6bb9bc05512679565aa5064262c6615a490e5bc310dad6454966b6b7d366cec662124ca181d87fbe9477c273ab67370905b381101d16c172db25c426ec67ab6816496fa4dd9c39f8e0aacbb5733b420bb0852e16dc82498162ee1af17895a31b9349ab12b24ad7cab2bd90f09fa05e6be5bbe3ff55ddc6e9aabe7e1166c6d9a185b41cc4bd92d30bbe8ad22e7e16ba81713ca43848f09daa3016030300b40c0000b003001861041a7029e2c163d016be67d1ebea3152d0d856d4653573ed568bd7af95822a3e917fb055bba3d542002f98076a1a33a1676027d1681942f1ec7879d142d1da2c5020bf1c6736e474521e7adb0bc54eb166bbbf5858ad9c003893181e85d5099483040300473045022054bd1c29531964ce58be532cbca41b3eb21ed1feffe12c304115426b49075e4b022100e774e4c7307201a3aa888c0124d37cb864d74348adc37ad517fd4b1b93329ba216030300040e0000001403030001011603030020c706b30eb002d166166f7ee2615593e2b732775d64987e479f4dd1cbef3219121703030016dc2968acedaf4ca65f766bb370d31bd02c4d7477873d170303001a2fe29fef21ea95c653c2f73e6db95bc124d25063197896b54490170303001bb1695c7364f78f3014af3adc101be66c85e89096740c7ebb492e8f"
22+}
@@ -0,0 +1,22 @@
1+{
2+ "random": "909fbf5dcd17ec11d2d0699a131ddd06f9a48e84f8ace25d5f49a56a86441e71",
3+ "session_id": "807c0bbdc809321bbcc1fbf8484956dde1bcdc0bb755ccf4924f831c734fdc05",
4+ "x25519": "6cf9f5b0d605e40c3a90dc550389ae865322c5b9146e0e6ef057cfb37eef4b91",
5+ "time": 1794462041,
6+ "lines": [
7+ "hello",
8+ "二番目",
9+ "third line"
10+ ],
11+ "replies_hex": [
12+ "6f6c6c6568",
13+ "ae9be7aa95e78cbae4",
14+ "656e696c206472696874"
15+ ],
16+ "version": "TLSv1.2",
17+ "cipher": "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
18+ "group": "x25519",
19+ "note": "OpenSSL 3.0のs_server(-rev)との記録。証明書はroot.pem・int.pem・leaf.pem(又はec12.pem)",
20+ "c2s": "16030100dd010000d90303909fbf5dcd17ec11d2d0699a131ddd06f9a48e84f8ace25d5f49a56a86441e7120807c0bbdc809321bbcc1fbf8484956dde1bcdc0bb755ccf4924f831c734fdc050012130313011302cca9cca8c02bc02fc02cc0300100007e00000014001200000f7777772e6578616d706c652e636f6d000a00080006001d00170018000d00140012040305030603080408050806040105010601002b00050403040303003300260024001d0020c2fa04185dc796fd031c133165c8fd0df109cbd33751dc603af176d9f4eb7d47000b0002010000170000ff0100010016030300251000002120c2fa04185dc796fd031c133165c8fd0df109cbd33751dc603af176d9f4eb7d471403030001011603030028000000000000000064531cb1bdb7fa978458da9346ded7749c22992f50d3d27ec981b9711f7e3f5c170303001e000000000000000151e33a8bc85a6fa348b0be133b21a07d8745efaa98f417030300220000000000000002dfced20f7c75e527c76c6838a9498da5e8c47b1f9e0338cdc96a170303002300000000000000031a8559b152f6751918de036eea9312b9327ed9463be9f65fa7f97e150303001a00000000000000045bf5358ad86903b85925915e4d2cb6a88b44",
21+ "s2c": "16030300590200005503036578b46f128a74becd858e381e22759f876f0763a68ee71a5b8877be4084f23920bef5ae55b9bbe6c6d98e6abf07c2fbe7a8237dbf9cf94e1a14a260c9ead3cbfec03000000dff01000100000b00040300010216030305950b00059100058e0002e1308202dd30820262a00302010202147fc3050cb7f8f64d8adcd3e3ceff89ca5c77a101300a06082a8648ce3d040302302c3110300e060355040a0c074c4220546573743118301606035504030c0f4c4220496e7465726d656469617465301e170d3236313131323033333932375a170d3237303231303033333932375a301a3118301606035504030c0f7777772e6578616d706c652e636f6d30820122300d06092a864886f70d01010105000382010f003082010a0282010100be7b519527a96ed4b3efb2dfb5c741c197d5e4e38aebd12ecb76e804ae3ac4f5d1eea82c0fdfd097baefd01b03ca1252ebd763d80853f7c8f58c3f25c3b323854b880d2d193a0a0c73ab4c9257d165a8783102a29cf00407f0f5ef88ab496c179488fc881886f0334d41007535b51c3964772186a6493fd09a4fed86e4befd1948a6f5f31082c741fddee56518c2a785dd8079c8be9f372badf73b8a8835f828e1048ee78077d6b168fd33d1f152e75c666e9ebe024dcf0ef85a7d17a390ee5d2454c6395bb4879e9ca323f39d1d47908f7c804fdb658a967b133121d56213934bf4b8a558872c8ce61608cccf4300763eb8fef2fcfd23fcbd5564c6cedd6f010203010001a381a83081a530090603551d1304023000300e0603551d0f0101ff0404030205a030130603551d25040c300a06082b0601050507030130330603551d11042c302a820f7777772e6578616d706c652e636f6d82112a2e6170692e6578616d706c652e636f6d8704c0000207301f0603551d2304183016801479a4bd3cce963e2beff81b068eb8c42e500a4baf301d0603551d0e04160414aa0a5ac49f5b8ff3859206d906c3566641a588b2300a06082a8648ce3d04030203690030660231009371adaa7413e49a2e8e9f7fc4c409eb419a92b808ff0d46d8f8d2940652e069af766235e4d4d7f71f1baec7ffe1a6ce023100f1ead31422f57aa827518410cd220ad1e5a94b1bc19815a2802e7e5213ce27942f1d181582e845dc573cbb073305f7e00002a7308202a33082018ba003020102021413d39c14d716af9e9061534acae06dd73063c6e7300d06092a864886f70d01010b05003031310b3009060355040613024a503110300e060355040a0c074c4220546573743110300e06035504030c074c4220526f6f74301e170d3236313131323033333932375a170d3239303830383033333932375a302c3110300e060355040a0c074c4220546573743118301606035504030c0f4c4220496e7465726d6564696174653076301006072a8648ce3d020106052b810400220362000405f0620c0b4c8a28a4a51abef03ad59fc95cd7485ddc867a54f89d3a71023626e4b769e9c8dc69991c8b68134f4f692a6a185ac7d34aa2176f4b37e0149111b6b1ee8afb50e3ee3ffaa5db28a217d214ae3e15fc83566d2a0f52db117be43d07a366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020106301d0603551d0e0416041479a4bd3cce963e2beff81b068eb8c42e500a4baf301f0603551d230418301680148b806c87a461348ddd8cf43968e998f486c2d4d8300d06092a864886f70d01010b05000382010100209459c6f2c5dda3bc5916b1209f67e9c3bfe84c5e2f46f3b440a46dee45252a6f9623b7615e2a4bd1094566a2ab476dbe1c0ca6eebcfd0e0436621233c909898a8d82baff5b1cca4cc70ee58b54336e617dff698bfacef8f01bfb0947b99bc9ce249251bfb2ed6bb9bc05512679565aa5064262c6615a490e5bc310dad6454966b6b7d366cec662124ca181d87fbe9477c273ab67370905b381101d16c172db25c426ec67ab6816496fa4dd9c39f8e0aacbb5733b420bb0852e16dc82498162ee1af17895a31b9349ab12b24ad7cab2bd90f09fa05e6be5bbe3ff55ddc6e9aabe7e1166c6d9a185b41cc4bd92d30bbe8ad22e7e16ba81713ca43848f09daa30160303012c0c00012803001d2088c44b12e431f7c0cba4e85cdca80faad65b67ee479b55f060ae7bf674edcb7d05010100afbdcbc3ea83a162de52a212d0c23d091bab3824d4e77f9b01091751b34668af64e82fa25f19d901caa89f3b015baad23fd1180e6fcb62d68a72646b09c8e8d0fe32c4cf547c0f3a2d31921088d03c135412010baadd03e622de0557bf9f30b31f582c0bf48b52762f3a63ebbd62e725c4b483f228bcad397176a1db69f7a020d0dfd3ff6116b0f9f563c713be62d5b74e6bfa166b2938acd8f74aed1a386e4c8a8e8561460d07cddd44e3b376ca108be7c562f9d9cf1d9b95bf8e92b8df721c257e7876c51e23ec0b7750a8a8434cdd70cf35a269b56a55c68341bcdede12af2044b38e7fdf79f741fdf42838f18353e5c2ce15a7b969ae91c471aefe0e664816030300040e000000140303000101160303002862389f116a8b6b0b748933f88b51428db5cbd2ecb54a3fc6d7aa279fd0b81394de820eb5878e97f3170303001e62389f116a8b6b0c9a65bdf326024546f1a7b895b56736ca0b6fa4cf7ca8170303002262389f116a8b6b0d897355f2cc149df9676665c783030d4a016868b507b61ae2f62f170303002362389f116a8b6b0ee2b096876a9a6f3463a920b51124c7d8dbd40a3c8a55bd5e396801"
22+}
@@ -0,0 +1,22 @@
1+{
2+ "random": "be0adda13a6a34f828cdb9e183ac1bb51b569f473b4c30675483aac2a876ef92",
3+ "session_id": "dc9aa9168f48a7d85434c534665ad423efe452b4bac40760617b0127f9b9ac38",
4+ "x25519": "fe718f03adfdea85532e915ca238c8418814d2f799eeb39625b79e66920dfca7",
5+ "time": 1794462082,
6+ "lines": [
7+ "hello",
8+ "二番目",
9+ "third line"
10+ ],
11+ "replies_hex": [
12+ "6f6c6c6568",
13+ "ae9be7aa95e78cbae4",
14+ "656e696c206472696874"
15+ ],
16+ "version": "TLSv1.2",
17+ "cipher": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
18+ "group": "x25519",
19+ "note": "OpenSSL 3.0のs_server(-rev)との記録。証明書はroot.pem・int.pem・leaf.pem(又はec12.pem)",
20+ "c2s": "16030100dd010000d90303be0adda13a6a34f828cdb9e183ac1bb51b569f473b4c30675483aac2a876ef9220dc9aa9168f48a7d85434c534665ad423efe452b4bac40760617b0127f9b9ac380012130313011302cca9cca8c02bc02fc02cc0300100007e00000014001200000f7777772e6578616d706c652e636f6d000a00080006001d00170018000d00140012040305030603080408050806040105010601002b00050403040303003300260024001d00200017b3fcaea9edaac5bb4d36c3d12c3c184f2b28ca4b77450366ce54e72d724d000b0002010000170000ff01000100160303002510000021200017b3fcaea9edaac5bb4d36c3d12c3c184f2b28ca4b77450366ce54e72d724d14030300010116030300280000000000000000c907b2530e56ad4c5a707fe77d5727058679e3d1fc08070c9050134604199aed170303001e00000000000000016af199bd3daa30aad80b65eddbfc231259243b579c9f17030300220000000000000002ce0d043fc7ffe70106877efbfe2bad023b04e76ce8e39134db9f17030300230000000000000003284b208cadfcd4eba4f615409a66b24449a551d10855b7fdf7b99b150303001a0000000000000004a362ad82af519809a9de7adbe53009faf352",
21+ "s2c": "160303005d020000590303970f38b6bdddbf5f3daafb47898e8b716fd769300a582bbccff53ff2c540cb1220717a7dba9e2c8e8e9ca91efee9c873983be4ea02513d5ecb71c6ecf35289fe86c02f000011ff01000100000b0004030001020017000016030305950b00059100058e0002e1308202dd30820262a00302010202147fc3050cb7f8f64d8adcd3e3ceff89ca5c77a101300a06082a8648ce3d040302302c3110300e060355040a0c074c4220546573743118301606035504030c0f4c4220496e7465726d656469617465301e170d3236313131323033333932375a170d3237303231303033333932375a301a3118301606035504030c0f7777772e6578616d706c652e636f6d30820122300d06092a864886f70d01010105000382010f003082010a0282010100be7b519527a96ed4b3efb2dfb5c741c197d5e4e38aebd12ecb76e804ae3ac4f5d1eea82c0fdfd097baefd01b03ca1252ebd763d80853f7c8f58c3f25c3b323854b880d2d193a0a0c73ab4c9257d165a8783102a29cf00407f0f5ef88ab496c179488fc881886f0334d41007535b51c3964772186a6493fd09a4fed86e4befd1948a6f5f31082c741fddee56518c2a785dd8079c8be9f372badf73b8a8835f828e1048ee78077d6b168fd33d1f152e75c666e9ebe024dcf0ef85a7d17a390ee5d2454c6395bb4879e9ca323f39d1d47908f7c804fdb658a967b133121d56213934bf4b8a558872c8ce61608cccf4300763eb8fef2fcfd23fcbd5564c6cedd6f010203010001a381a83081a530090603551d1304023000300e0603551d0f0101ff0404030205a030130603551d25040c300a06082b0601050507030130330603551d11042c302a820f7777772e6578616d706c652e636f6d82112a2e6170692e6578616d706c652e636f6d8704c0000207301f0603551d2304183016801479a4bd3cce963e2beff81b068eb8c42e500a4baf301d0603551d0e04160414aa0a5ac49f5b8ff3859206d906c3566641a588b2300a06082a8648ce3d04030203690030660231009371adaa7413e49a2e8e9f7fc4c409eb419a92b808ff0d46d8f8d2940652e069af766235e4d4d7f71f1baec7ffe1a6ce023100f1ead31422f57aa827518410cd220ad1e5a94b1bc19815a2802e7e5213ce27942f1d181582e845dc573cbb073305f7e00002a7308202a33082018ba003020102021413d39c14d716af9e9061534acae06dd73063c6e7300d06092a864886f70d01010b05003031310b3009060355040613024a503110300e060355040a0c074c4220546573743110300e06035504030c074c4220526f6f74301e170d3236313131323033333932375a170d3239303830383033333932375a302c3110300e060355040a0c074c4220546573743118301606035504030c0f4c4220496e7465726d6564696174653076301006072a8648ce3d020106052b810400220362000405f0620c0b4c8a28a4a51abef03ad59fc95cd7485ddc867a54f89d3a71023626e4b769e9c8dc69991c8b68134f4f692a6a185ac7d34aa2176f4b37e0149111b6b1ee8afb50e3ee3ffaa5db28a217d214ae3e15fc83566d2a0f52db117be43d07a366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020106301d0603551d0e0416041479a4bd3cce963e2beff81b068eb8c42e500a4baf301f0603551d230418301680148b806c87a461348ddd8cf43968e998f486c2d4d8300d06092a864886f70d01010b05000382010100209459c6f2c5dda3bc5916b1209f67e9c3bfe84c5e2f46f3b440a46dee45252a6f9623b7615e2a4bd1094566a2ab476dbe1c0ca6eebcfd0e0436621233c909898a8d82baff5b1cca4cc70ee58b54336e617dff698bfacef8f01bfb0947b99bc9ce249251bfb2ed6bb9bc05512679565aa5064262c6615a490e5bc310dad6454966b6b7d366cec662124ca181d87fbe9477c273ab67370905b381101d16c172db25c426ec67ab6816496fa4dd9c39f8e0aacbb5733b420bb0852e16dc82498162ee1af17895a31b9349ab12b24ad7cab2bd90f09fa05e6be5bbe3ff55ddc6e9aabe7e1166c6d9a185b41cc4bd92d30bbe8ad22e7e16ba81713ca43848f09daa30160303012c0c00012803001d20696ce78f3c19baad70a4fd8ec0ae850fe219ead4faf6612231c818c8e7e5e83808040100ba273563db4cf4e86d9dae58eb7bd05c60b58b1e08f3ce51c16c5da64e088df74f880b281a06251abab4943e8fcf42d64e495ba29e48d4acfaa9263bd17c05bcca629fb2d200ca0894e4583051300e838f9be3488bf67cad714ebea11e7decd59c86948a210e96bd17ebed15d1c96812e602463b341182e919d631d3db8e886b3d2bacdf628b217cf11181d6e5c7c823f1485ae5ffb5256154cee7c0bdd5175d8e38c4043253b6af90b7a26064a7b574e4816e6c9ac23b8897ee9fa95a7fde23e83d37793177d4c98bfafba71c924737aac6ad0370a8a81430d961838ded1caac0d060fe02c80444c04e2fd40022b6a8d74305a6ac629821f2db1e8d7c94e17a16030300040e00000014030300010116030300289f0d05373125d9678c026852161e0e473dbf59c5092ebe93e8c1acd2c0852c52ee8ab0f87f103e44170303001e9f0d05373125d9688b3338384ff2d67b62e37619f8de37ce567c48ba45e417030300229f0d05373125d969c7929121bba34f17423a70c9201a49ac0666c89057cba304baf817030300239f0d05373125d96aaba5b6dd8c906c0d008aebca6632fa4ad315cfa123ad67c27c4bf5"
22+}
@@ -0,0 +1,22 @@
1+{
2+ "random": "21eb019e7d02d85dfc72540cf74302ed937191ec921d246c1d414ef15751edb4",
3+ "session_id": "8c74025e1cf5fe63d513e61b6365ca5e019b446d2535f474b996b1e7c51bda3f",
4+ "x25519": "ca206fa587e90e49481ac798f18b44d65fbed4760dac225d75b1eb74766e3fe3",
5+ "time": 1794462036,
6+ "lines": [
7+ "hello",
8+ "二番目",
9+ "third line"
10+ ],
11+ "replies_hex": [
12+ "6f6c6c6568",
13+ "ae9be7aa95e78cbae4",
14+ "656e696c206472696874"
15+ ],
16+ "version": "TLSv1.3",
17+ "cipher": "TLS_AES_128_GCM_SHA256",
18+ "group": "x25519",
19+ "note": "OpenSSL 3.0のs_server(-rev)との記録。証明書はroot.pem・int.pem・leaf.pem(又はec12.pem)",
20+ "c2s": "16030100dd010000d9030321eb019e7d02d85dfc72540cf74302ed937191ec921d246c1d414ef15751edb4208c74025e1cf5fe63d513e61b6365ca5e019b446d2535f474b996b1e7c51bda3f0012130313011302cca9cca8c02bc02fc02cc0300100007e00000014001200000f7777772e6578616d706c652e636f6d000a00080006001d00170018000d00140012040305030603080408050806040105010601002b00050403040303003300260024001d00200ff070bf55240544abc5cdb91228f73b171bca23e27d53b34bc474472b97914d000b0002010000170000ff01000100140303000101170303003540815d38604801f3ff925d02befa4863787b62eb543bcb4f032303ecb3dd9d3cea32349ce1d73127652b42fa4a82a25ecb70cbe71317030300175edaf75bd3115c8f8dce3c21aac74c2c6808eaf2198420170303001bf42962a0803e585bdb9256d325ee780b038182e71ff55a5b83cccf170303001c256be6a2ffb39e72afcd6ed6b6c8e8576270dd19dd9bba0ae6365a871703030013df37ca34d1a91e465efad382f223ecb8bbacfd",
21+ "s2c": "160303007a020000760303a694c29fa519b76d5d8cc1b751e065e2e7611305a311fa889fe5f8a36e218695208c74025e1cf5fe63d513e61b6365ca5e019b446d2535f474b996b1e7c51bda3f130100002e002b0002030400330024001d0020d678025aa25bd2e8b856fe48f9f1830e20644dc91c1e5dead83e9c94215ceb041403030001011703030017554675ce811aed92f4e817a5c3d498db473ca114cd105b17030305abf444c53759222a897d723231301551ed476ee3cd681c8929b68a8748e3e53294b900f9622a1d9923480774bee56298f67fe1a2db33ad3b1992a86569a1628244bf1faf72a99c8e6e05b1649ddb8c3828e64f33bfbceda887e451f3f687c00f83f003a4c94bd086763fc72408086afdf1943f5eeda9e09ae43a8b157a7b6cde4eb39f1e90e7343e6c82eaf4ce55c7287113ff3f864ebc9353f2c5c438749eb930119afdae7c8682c6e874ae596909a95aa59515fa8d0e101601c9c4f668ad78a041c5a5601d4676c8e9bd978d7745db6bfead32ffce87c68ce3e69a34cd51774ef1397a6700783fef69bbed912a5bf8574956783562063b5085f5d20a9f2db5fce87e6210d825b8e174687bf1ab68b95fcca0f345d408a697ab6422c4090c0ed272a4c0c2bde4a4ab832a3a936be3b8f1329dc807fd39ca72d4b2dfcb9f6cd92c0d4f16d2341b0a208531a6ddaa027e64de5042094a540bb607ad7285b13ee1ce8c09200e648a60cd6b0be9d40dcec8062d53011b6e6c3b6612e47b1f1153b6ca30ef29516bee52e43e363039f4bf0387a762f19312adc7e19069c2ae4ccf27c793e5ccafa78f7d2c35152c08bb92d0976b30eaaa6b0c4d46b45c88c820bcf44d6964c7868dc646103397e76adf0ab6ab041410f1f4d57cf6580f33031e940e0ca61ff2ae8c8eb8b39d2a3fd1dd89ac48e63cb777ec7a4771d21d2861ec2da04e97621c7e6f4100fc288a0a3a484311bdca6ebb931241d346d1eb7b026dd2d9dbe6ef9e00eb8fbf2913fcc40c26129fbf2f2996eb101d444f27d66c46c93e9beac768f84c3d50ab8d69c0c6051a16fb8eb187dae0c0629d744f234f17a05e0a9f97db776972e9d0f0b1f408a7a97a1b019089763c258fe2a6d1ef1116c0a1386ee24c72d8895e17e49bfef0a0ff28b35396818271a6d7747bb878cafeb0aa7ace19196d8f63bca6cb6ace6745f1305fa8d80389191a5a49d77a6c125521ddd64be62329fbf91ec2aa8bbdd9ea56a48f466e13fded2d838891a248d4273cf90bf91b5e6cae571bd1950fbfb78772da20f6524c924cca8a2442413d32ce889b0de4d9ea75c6f823f602cad734f212d17c8d5c2c5ef3859a76afe1054a273337681c07e7e694076075c31a306786cd2ebefed0e77dfa47f4b11bb8c1c32be1ae75acc1667a91dd9004f239f9f8cf465719d41c3f27386d054995afc4234ad79198fcb3bf6370ebc4af618007c0904925d6392304342d6462e04103b85cc54e681ead5ce6abebf297b747d7e776268077bccd282d753cc37c5205076d1c80e4cf46aede6f1db204c203b5c230c783785b3a1e49cd0f9056ac6fe9c5c3309b83796f9d96f6ab4d682f267e2fcef83414c3b5f086bfe24e51b279554a0179e840f79b41fb1bf689523249f2b5ed86d451afa651a2b9192c4ea78e18b05b8cb8609e6c99838a3c5fdc343460d3008110b6dfdadd0dff66ca973998120681cc6956e4833f26e470723d9904f2c484a913c48f374e0eb1febb55baf018903ca05adc6a4bcfb1ab29353e67e34be556c3b9d8313717c7359889bddbe6c18d21903cabe5792e526eb2535512f6d1fab12da2161dd6c4fff254c10f498cbf1f29f638a3380b141627c3bf34f1570996e65307e83eb812fcd2d994ba4ae5ab4724b6eb4f62dad925e52298676985450c3431ee71e0ff2f00e57cdaba230e34a9ad1b473b38469f025b6657dda9381d53dd727707d3a564516290964e0bb31c389eef9f2e843f0d51220740b21e137f0c00af9ef45389b054571c82d7f080f50fcc7aef71b50c0509eab3a2fabc8b449773af4000cb0722524d72f5e8d0f8a53fdd38e41f200fe3e0af6522fa66d10330c8808990729fa87d65121ffa0ea79bab45fa8e342aa0bd73d102f7288985633ce917d459746d1cf0f351a7a66148692cbe2dca9c9d0ff4c75fdc23d33efa934bfb56808beb2c637c4dd158c6d501dfcc90b56fbf0a121fe7bfbd33749f2b61e5e061eddb9dc1e3ddc2826984157af5ab108924c2022ef2b7a769c14e828b4bf2e16017030301195493c50205465b4d18cfd207ca658d7647d70ebee9350d36581d78f11a1056b6e4b5cea31e81b27d7a28dcf230ab99c89c721a8d16d8c7278c7e7e7c7593bd7598c4e4b016131d15f8ec53412bf138f60662dede71821f10601e4519713f763b96868552b4909e8d64566f8f2c7cd77fa13261446d77f5092264cf4505a9c39ea350d4e8780b5f8d1c95a0a766f38917bd49764012aeeaa442fd52aad05462e33bd268a25566e77b729cb3cc2348613ceb0feb1e1e313c06d6145a6544a13367233007b87b2cd48d8859b2ada04ae2bd84730151a78e504f69c5f7041187382744621370254fe93899b16068f251212570cefdfb8bb14d59285ac538e6ec8214b51669fd9e2d39529043567a96c28f9385923431945419bd831703030035df3f2132ecf75ce6eb606609a796aab04856ea151409c98a66fcf21e6deaf51cb88e5327cf71c64d9f43c2a7f0ba7bc1d015be91ff17030300ea9f80684fc1c57d62d45c33e1a21d6bf9db36aed2e5b1de9cafd0cda9dd8132f7bd1ce1b0a09cd48e3c99659101b2a544bcac5cdcef50acfdbeb9abf9ee94a2bf3020b4426576ddace9c7d24abd939529917a1b94dc0e6bcf7de5d41dec4482e47ca61622d7855f48e9daf7b284ab2a9762a0ad5810f706fa9e5fab76faeef9d9f8d9396abac2727f8aeb56ec12a12dde733a437dd288e73f068bf69848b4b3eaf6d9e6245fc6e5fb299c412933ad4f777de48c1ab6af7487170660dfce2bdda2ad3e4921d928d7d537304c1046f1074efbf0d532641497e4dddfeb709173bd58f1660718bd683611d28417030300eaa1e7ba580eb6fdc168907e0998c9629e86ebd50d75a7da42ce43bd1adb57ba5efded6c3e44903ec5eb76a82583c62fb69b399b2e4b510ef2f972c054f5403f8d357d42e74bda82926de34c99dbd228d83c0d0d1aa315d6926b63b624b8809841fe8cadc70becc2b77623e468ac97e656f80eb4b13c579303e56dc3034f464d4bf760a6f99793ddfcdb9d6a0497b2671e8a7cce2c6ca477ea21a7dafa7b1b78f6bdc54d98e26eee7bb019d61c2fb9f9cce9c52458d010c25f563c93d2754619bdffbf0151f698ad96fca2bafeb5c1ca49f5607f3766f05427155d4cbdac5410684fe31e7614015d3229c4170303001789ca19a6681a4d1ef79ca166e5d632951a9ac4e39d9d1d170303001b1ee0f3ddcf78605b22373f13b25912bd27069141f750f8b9c8d0ed170303001cdf7f138378a3e14d2e87b9f064e27aed615b97b4f544157de43cdd93"
22+}
@@ -0,0 +1,22 @@
1+{
2+ "random": "ecb95aa3e5c126fa1b56cc294801dbffb5a69778677e60bf79789b056b8b027e",
3+ "session_id": "d884d858bb1458ebc539036f9d6e68668d6aa9bafc3405cabda1828607a1cbfa",
4+ "x25519": "e463a2bee398e02306a1a2a1bfc8af9a5d0136229bc58396a1fea41a2a72bfa6",
5+ "time": 1794462039,
6+ "lines": [
7+ "hello",
8+ "二番目",
9+ "third line"
10+ ],
11+ "replies_hex": [
12+ "6f6c6c6568",
13+ "ae9be7aa95e78cbae4",
14+ "656e696c206472696874"
15+ ],
16+ "version": "TLSv1.3",
17+ "cipher": "TLS_CHACHA20_POLY1305_SHA256",
18+ "group": "P-256",
19+ "note": "OpenSSL 3.0のs_server(-rev)との記録。証明書はroot.pem・int.pem・leaf.pem(又はec12.pem)",
20+ "c2s": "16030100dd010000d90303ecb95aa3e5c126fa1b56cc294801dbffb5a69778677e60bf79789b056b8b027e20d884d858bb1458ebc539036f9d6e68668d6aa9bafc3405cabda1828607a1cbfa0012130313011302cca9cca8c02bc02fc02cc0300100007e00000014001200000f7777772e6578616d706c652e636f6d000a00080006001d00170018000d00140012040305030603080408050806040105010601002b00050403040303003300260024001d00201365ea658cd352eea2039f0a6b106a698c01cbda8522be5092f9595e2342a935000b0002010000170000ff0100010014030300010116030300fe010000fa0303ecb95aa3e5c126fa1b56cc294801dbffb5a69778677e60bf79789b056b8b027e20d884d858bb1458ebc539036f9d6e68668d6aa9bafc3405cabda1828607a1cbfa0012130313011302cca9cca8c02bc02fc02cc0300100009f00000014001200000f7777772e6578616d706c652e636f6d000a00080006001d00170018000d00140012040305030603080408050806040105010601002b0005040304030300330047004500170041046ecbe91b9c300bb6ac4d1612508780ee4574d5a2856b89391c7d531d0d3af9c9e41cd7b4cd7e65bcbcbb87c267fc90612c56afc15e73de237e61cf007e97be76000b0002010000170000ff010001001703030035acbb5254ca03951c9d6fc905fa4e37f003900847862bc8fb7c6b2fc03138f0b11081396106865f3315dc764843b9d5e5c7f17c1a211703030017dac84aaf4dd717a7081c0120bd97e6b78577ddfecd0066170303001b8c177e6332c97a03f3e6df64493cf40c96da9f85c79d06a4b6b59b170303001c941acb5bb6493982d71941c4d76ae92a7d0e566a72eef27daad1cded1703030013bad35523bc5f4a60bd3d4a7bcff269e44650b4",
21+ "s2c": "1603030058020000540303cf21ad74e59a6111be1d8c021e65b891c2a211167abb8c5e079e09e2c8a8339c20d884d858bb1458ebc539036f9d6e68668d6aa9bafc3405cabda1828607a1cbfa130300000c002b00020304003300020017140303000101160303009b020000970303989eedc4a5f712be60e52e55e9fababd7351c5d91de37f5dc5433142a9353f0120d884d858bb1458ebc539036f9d6e68668d6aa9bafc3405cabda1828607a1cbfa130300004f002b00020304003300450017004104c07f2abebdd2205724d4178c2779ad25afafbb159d599e9f93e7e44d4d39d13b731110b05f46674c2d03733cf037e1c462b4ca031dc095ee0056592823fdd5261703030017d972bfa27fe7a62c91d0c4f5d333dfa8ce3911aca0be2317030305ab1f68dc4720ee0af1eaf1903cdc17b514d1214d35af642677463e3eaf32ca336bd6b85f36c1cf38cfdfeb79318675579624662a61babfebd40d5c032da9be5b69547a0715d2f879033a489228a88e826d17269b29e6f6a5c31cf49123d9352b5f4b439b1898150846a9066763509652079e5a38d7ed0269d44f4f4c79df9a425c2d526c8ab51e5ff1bcd7aac1dde5c6db53e67ff2d2b45fb2437b75f0a37c6410f860ad3a5db01a97c5866f95728407abc08617fbec0bc4bc33f8aba43c950a8db96c77e79eda225650aa70d86ade214ffdee017240cf3fca1bbb242e6e7221ae47a9cf9daeb9f7bcce46a2a470702bb23a761304ed960307c3f83e934d7ce8dc5ab25883c6dab7861c6dbf5db8bf7fa28342c230ddf1ddf8dfaca84d107b731a01124fe9429c159600164ee31d769d7d33e761678dc55058ce1c5bfbad42968af5e3484ef33adce29fe5e3ce7476374f4038ac9c0be05821951d75f4178afd466aefe1d094cf9eb7ce4a4587dac492cdf9f1fca236c1c60dac4fc589e00714406811cb591d91ed3f1c9edfa06e927eabac2c45e9aa4b20b57b96b3ce61c1fc701f661ea2aa2d0a650bd0491756510aeceefac1f5bd90690ad62e765671745669d60ad4aafebdcf20f099adf927e26fdafce204b6ac575e3799ffa3dfacc0df6c837b0a04c62379f3139a8fd1c4667c3bd41ff0dc6282125c5f2ec6c66ed11e0415be6fe8be80e4b5ab22f8310c76bc2655bebd33f28659ebd9e17f93271627a9d3ebc56ec24a07bbcf045017a6aa3de3afbd40fce10b4e72c2c6b409a28b88b6028cf009406e0c1b5cfea90f711a59d90403278b93c91a8948d82919b85d0ebc53289ce2d9bd9c027a0ab842491c6d5d498f705440d592c7696b0f41d6891169217ea28e427651ce865e6f1971975e55e6ebb1ba54aeeea92df82a4784f334d0f796e03681c4ce2ace10f95df288db79f3c061227013b977cd603e0d899f3d934efc669629fc828a49f846cae5c4b75a39a54c51a7af74842de555f8a696cf14798b5343ea2d671b73f3581eca6d0d4362655e6aa7a0ae2adcba0363d46529da1210d5e15467bdc1c7813a65da61b45f3e5f356453a93a01951e1a3bf347aeec46fbe438e55e1ead729622903fdf45256b0aacb87762e0e3bc62a1d2f70a6a9827c4cb413d424657483a437af83d1db5e5a4af1e38f19a4fe2d670a6fc7ffda5a8502b0372b2d426c438a6f9b7736a1bc8a45b1e014c896c70fd829a7264b0a8d476a267163236002370a496dfb933507ce5c4bc111c478d65250d7fca01163bb8d784cddd5a68d7e9c548c37b6f4f6c8eb531881daec4c63b046134e1a7a0f433d7dd1ac310caebfedfe21e9b581778f368aca80b621784af0bc48876b750dfb2875ee13cf91a433ccfb729ed996cf81884e0a53a539bdd065e4f78b1af0ba2d65aac6b5a5b080ac8a6571a691ceff8103ae5eba18f4f143e020218f11d4e4f6017f48b71ff9a70e792748575bfd790a8040d7e923dd572c830c5cf66fc31f9eca55b8b885a54e6f5c4308cae6765d2e8f5ab839b21083a341a269a393eba3fbb49755fe0e5339c4a9ccb3a889c24bb34f022a849450baf8929f0402ecf9f90915b9b17ca730fecf8b5f61ce3d72561c009c7088772b475c0cc3dc88411e1e771e22b6d99b3fe4db2af7d1871b7e2c711fce19939b31117981eba06336d6f422a372c5c2ebf6d1fb5abfe9e746f6a7234949529cb2f1c8b840ca1fac09312b81072c01d9f4195827001815b582c498825d6db8858fa83f15f5d819b887143df925cf962567ebc7cae235c8cf580cc64d3d75fceee133c45dbc61a9df927ce531adb7dd563bd9e7142fc65cc1a8f66cacdb2941347f47d6c2ca4ade739ba675ed3417fdddde004f17813a3b1beccc19a949c5bb9f04858f0a968755e04074cc2e6e51f03813c12ada93b1e9b7c883c697e3592fc259680c0fdc5edec391452af348bb54064189d7c23a3144d58118c315445ee23043491f22e79e5003161f6734abc1a57e8aef8a4703f4f170303011929366bd0733cfe7cf76efe657da75c208641388730ee70b3a25822d8afe3de8a21c697f8e00192ab7ef442ed331aba1996f9a082514b4d0cb751dd4a24fc3bd58c4f8ff13c1e03adf3b8353509da570c4dbe574d5c0312d7455680ef8a14cb1e1e274be353279c275cb4fd2bf26437065a022da60a7dd4f39f2cdc53eaf33c6395ab90c3d233a47c8c52b12207ea41452ec4e84b9195a1e3261d809e087d6bb165a3eb3cbfb89c71b969376171abfc4d18633f6fabd0df51b56a7c38fa7ca1e9aa54fae2c39f2699573c866fca12cb9f06ec9e3c6fdcd908627ff1edcea1025461e00d9df84c1f09f50941ffc3ade66ccb4ce2412d397dc33b0470116065ba8f03f58630a7991fdc436371251a3224cafc088f9b959363025f1703030035e69c4ce43edee9def870789eee239d5cacc68f7b6e6fbd0350ccc6b8754c78dab7f337479bdc3338a3e3e86d37bdb74f4ffd9785dc17030300eae43ae1d1bbd06481b7d6ffa5d939a3b65d06de457e1def500a11b3ec02f67f4876f05d226c14c822122ee7731e1211bfc32c6e83f3178cfd17b5cd04f41cccafe81feab51f2751ee3f81fbe9792831dd22e4d84793a04853ef4173fb9994a834fc6025c8fba3b6189c8951debe80dd1a70093a5d8819a364ebb3b2411b3c83caf1fdc02de4e2fc20c2faefca5d914140c5d390ed7fd0955d853afdb1c7464de15bf250069192dbb0ae424444471b6b36318fe87fff8172ecda8aa1f11831869a1689a8fd5a5f07056f785c9442f8d7721518f97be2b3b6b8b20b09cbe099d1d2ebcc2be71c2ccf52515e17030300ea6228895c5b11138ccad2c11024fccb5afa4d54aec413ccc455748aadfae400e040aa6a19e6065effad82276446b5ad8d97b8a069fc752e92362214f3613b37715b8e82c84bad144162b3fe9eaf9fbf3598deae594a86d9cfc414a28b678893743d43e1a297220a0900c6bbbacb528e5d3e1e84efb1b5c88249f2dc102de3ebf3a3abfeb25d964077fb76a9efbf7fc0691e23e49772e0b2cdc5d2f810e018b7fc0a89d198698bdd0425064159d40d0d9b81368ed1d153ef2b02aad01ac4c22495e15d7b5f686158e87302a0f8dfb451e53ec120f3f07833a72973e5f8f2ee541cd032cee048a58eaf744b17030300172d3b4da4308c1a86fcc96804f7eec9c933d4dbd74d43e1170303001b508840866136eca899844371d329dfca55144545364ef7408bb4fa170303001c11f2ee48a581a06b72f7fabd4e5cb8d8b90582091dd5dd08077b7b18"
22+}
@@ -0,0 +1,10 @@
1+{
2+ "pkcs1": "a7462a975e5a1da626c0c6cf2b69ad386ae1e54f33b2e57a28dd78d34b51b0887842f41aec052818240954cb4da001b6b6aef68897b52483c1e0bec9d401d8f1399be7cc56ef77fbd4652b61136ac2680ab51315b20d40af416c9387b1df1c92fd6888c964c2262d057f275b99744ac55d6f14de6fb8f84a952df951b5a7f6b3637c15506c667063328c339d185b85f473cbd0561112a96a4625688de799145c19054e5a3e34214dffbbe1dd0bf6905ef6a2204871fb35d98dc5cbd311bc8f9ce66f6210b4f83fb6aad161f99dcf725436267e6a1f4ab3ec0cdfe18857b36286878b3be59062b0720ba15a435d100db2e978ad8ae4a37e5bed9fe6fc5b5c9c58",
3+ "pss": "55aa19da6e25c6dd2010a77339d26453082b564aaad75bf7e0967784d76d96b65ca95aeeb48f20575995da9395b8eca1f382d450aa9a5c0782061330c5e1f3be1e976b02b1d26a69f312435c9c7d59077d8644b30c9f6d1c4fb6184be7e2db704e14aefccf62475c904acc74ee950deab27ec0fe22df319fd9c87456913463e070e72d8534731fcb63e1bdc34e66e7e183c6ea52c5604698c45ce36cc002242f0a805d56badf613f73bfb7d41e814d7da793f66168fea7f6ba3095ffa98eb67dc18f06045d435d1dbc6906fc8cfe179adec8be381854c9ad29bda678a03098e955015697ae6e631dca3140be8f05525d8dd7b23286b18cb145f89fc92424d9fa",
4+ "oaep": "2a48120bf31ac2024a3082e3066faaeb42e68fe9f23d24b786e70aea5fe9837a454e5a9fdf53d475ee48fe7c762e948ea3e28026f9cf4a52a6576837f47bd22eb477ec5061085d74314992e387f8bf6c56415c8c19f32d3133f6f4e4c3c4b03dd58813bb37845f36fcfb7f6f882c22ec3e318fbb9981f2a85927f4bb38fc65b6982c7356aee1477d988042f2e50978be2000d4a900c9bbd4e6a005d7d4271fee67ba9c264bcd898c50ab05697074e5541e5f86cd4b3572718c0ae926d34a3794d0c3c53cb8eff82889fa1c024945ec45931b4848fc09b0b6943b02dac73b4ee9f50a0373822dfe0b16c11681af5594b0add488ef3b02b542fab63a741ec89cde",
5+ "es256": "3045022100e581675d012efc4f554db8937b10b0c5ead02bdfd37de08fc0f9307d94028d4f0220624e0b3ff53bedcca9ea6ae23a2e939820f4184ef44404b2be11f5c0dfe794d8",
6+ "es384": "3064023066c44cf6ee03bbe0414d66850b6a9ee55a7f8bb15f383719603ec95ac6755d27f132f5c7ce5b834edc4a81cf44e891850230384d83fff2d8466d1a34c32de9815846e56511aa81ec68767d8d8f680ac067515c96c475661cc567d65cf1a7348b62f7",
7+ "message": "076の署名",
8+ "time": 1794458367,
9+ "pss0": "285866fce2607350ecc70e6dfdf613870fb0605b16793f2dac823bf37aed1c38243bb6f683c735c924b9e65847d72842da51c708c5fa8a7186a7dc33943863a691f98ad306d2983bfa31285d619915da33048065aa796e6fee3d460af2c58b4132c2ca77a5a624f08b1dc82bb7b92a0ab26f54652c60b8c355ba6c967976aeb14cb800c11466498fa510dce42fb941c979725bc152f4c26f9197ae907db57516fef31476c1730e0076e2b858572090e91f4781ad619d9103618fe0abea0aae8b2a1a64f937339f77fec5f2d8e388e8176071496e7e070b88bc7935a59de1f4c87b842780cd5157513038ed9cb2a60507c4059b1cbbc2059ce515e09a5f164278"
10+}
@@ -35,11 +35,16 @@
3535 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
3636 OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
3737 */
38-// テスト用の偽物のSMTPサーバー(MockServerが起動する)。使い方:php smtp-server.php ポート 記録ファイル
39-// 受け取ったコマンドとメール本文を記録ファイルに書く。
38+// テスト用の偽物のSMTPサーバー(MockServerが起動する)。使い方:php smtp-server.php ポート 記録ファイル [証明書 秘密鍵]
39+// 受け取ったコマンドとメール本文を記録ファイルに書く。証明書と秘密鍵を渡すとSTARTTLSに対応する(Std\Lib\Tls)。
4040
41+require_once __DIR__.'/../../../../autoload.php';
42+
43+use Std\Lib\Tls;
44+
4145 $port = (int)($argv[1] ?? 0);
4246 $log = $argv[2] ?? '';
47+$tls = isset($argv[3], $argv[4]) ? ['cert' => $argv[3], 'key' => $argv[4]] : null;
4348 $server = @stream_socket_server("tcp://127.0.0.1:{$port}", $errno, $errstr);
4449 if (!$server || $log === '') {
4550 fwrite(STDOUT, "error {$errstr}\n");
@@ -55,6 +60,7 @@
5560
5661 $first = true;
5762 $quit = false;
63+ $secure = false;
5864 fwrite($c, "220 mock ESMTP\r\n");
5965 $auth = 0;
6066 while (($line = fgets($c)) !== false) {
@@ -74,7 +80,18 @@
7480
7581 switch (true) {
7682 case $cmd === 'EHLO':
77- fwrite($c, "250-mock\r\n250-8BITMIME\r\n250 AUTH LOGIN\r\n");
83+ fwrite($c, "250-mock\r\n250-8BITMIME\r\n".($tls && !$secure ? "250-STARTTLS\r\n" : '')."250 AUTH LOGIN\r\n");
84+ break;
85+ case $cmd === 'STAR' && $tls && !$secure:
86+ fwrite($c, "220 Ready to start TLS\r\n");
87+ try {
88+ $c = Tls::accept($c, $tls);
89+ } catch (\Throwable $e) {
90+ file_put_contents($log, "TLS: {$e->getMessage()}\n", FILE_APPEND);
91+ break 2;
92+ }
93+ $secure = true;
94+ file_put_contents($log, "TLS: ".Tls::info($c)['cipher']."\n", FILE_APPEND);
7895 break;
7996 case str_starts_with(strtoupper($line), 'AUTH LOGIN'):
8097 $auth = 2;
@@ -0,0 +1,159 @@
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+// テスト用のTLS 1.3のサーバー(MockServerが起動する、Std\Lib\Tlsのサーバー側を使う)
39+// 使い方:php tls-server.php ポート 証明書のPEMファイル 秘密鍵のPEMファイル [ALPN(カンマ区切り)] [グループ(カンマ区切り)] [悪意] [最大のバージョン]
40+// 悪意:bad-finished(間違ったFinishedを送る)、bad-cv(間違ったCertificateVerify・ServerKeyExchangeの署名を送る)、
41+// downgrade(クライアントのTLS 1.3を無視して1.2にする=攻撃者がClientHelloを書き換えた場合)
42+// 受け取った行をそのまま返す。特別な行:
43+// INFO 接続の情報(JSON)
44+// KEYUPDATE 鍵を更新して(相手にも求める)「updated」
45+// BIG n nバイトを返す
46+// CLOSE close_notifyを送って閉じる
47+// DROP close_notifyを送らずに切る
48+// GARBAGE 壊れた(復号出来ない)レコードを送る
49+// HELLOREQ TLS 1.2の再ネゴシエーションを求める(HelloRequest)、その後「asked」
50+
51+require_once __DIR__.'/../../../../autoload.php';
52+
53+use Std\Lib\Crypto\Ec;
54+use Std\Lib\Crypto\Rsa;
55+use Std\Lib\Tls;
56+use Std\Lib\Tls\Connection;
57+use Std\Lib\Tls\Stream;
58+
59+/** クライアントの確認をテストする為の、間違ったメッセージを送るサーバー */
60+class EvilConnection extends Connection {
61+ private int $finishedCalls = 0;
62+
63+ public function __construct($sock, array $opt, private string $mode) {
64+ parent::__construct($sock, $opt, true);
65+ }
66+
67+ protected function finished(string $base): string {
68+ $f = parent::finished($base);
69+ // サーバーは最初に自分のFinishedを作る
70+ return $this->mode === 'bad-finished' && $this->finishedCalls++ === 0 ? ~$f : $f;
71+ }
72+
73+ protected function finished12(string $master, string $label): string {
74+ $f = parent::finished12($master, $label);
75+ return $this->mode === 'bad-finished' && $label === 'server finished' ? ~$f : $f;
76+ }
77+
78+ protected function signTranscript(Rsa|Ec $key, int $scheme, string $context): string {
79+ $sig = parent::signTranscript($key, $scheme, $context);
80+ return $this->mode === 'bad-cv' ? $key->sign('別のメッセージ', 'sha256') : $sig;
81+ }
82+
83+ protected function signKeyExchange(Rsa|Ec $key, int $scheme, string $data): string {
84+ $sig = parent::signKeyExchange($key, $scheme, $data);
85+ return $this->mode === 'bad-cv' ? $key->sign('別のメッセージ', 'sha256') : $sig;
86+ }
87+
88+ protected function parseClientHello(string $b): array {
89+ $ch = parent::parseClientHello($b);
90+ if ($this->mode === 'downgrade') $ch['versions'] = array_values(array_diff($ch['versions'], [0x0304]));
91+ return $ch;
92+ }
93+
94+ /** TLS 1.2の再ネゴシエーションを求める */
95+ public function helloRequest(): void {
96+ $this->writeRecord(22, "\x00\x00\x00\x00");
97+ }
98+}
99+
100+$port = (int)($argv[1] ?? 0);
101+$opt = ['cert' => $argv[2] ?? '', 'key' => $argv[3] ?? ''];
102+if (!empty($argv[4])) $opt['alpn'] = explode(',', $argv[4]);
103+if (!empty($argv[5])) $opt['groups'] = explode(',', $argv[5]);
104+$evil = $argv[6] ?? '';
105+if (!empty($argv[7])) $opt['max_version'] = $argv[7];
106+
107+$server = @stream_socket_server("tcp://127.0.0.1:{$port}", $errno, $errstr);
108+if (!$server) {
109+ fwrite(STDOUT, "error {$errstr}\n");
110+ exit(1);
111+}
112+fwrite(STDOUT, "ready\n");
113+fflush(STDOUT);
114+
115+while (true) {
116+ $c = @stream_socket_accept($server, 120);
117+ if (!$c) break;
118+ stream_set_timeout($c, 10);
119+ // MockServer::stop()の終了のリクエスト(暗号化されていない)
120+ $peek = stream_socket_recvfrom($c, 4, STREAM_PEEK);
121+ if ($peek === 'QUIT') break;
122+
123+ try {
124+ $conn = new EvilConnection($c, $opt, $evil);
125+ $conn->handshake();
126+ $s = Stream::wrap($conn);
127+ } catch (\Throwable $e) {
128+ fwrite(STDERR, $e->getMessage()."\n");
129+ @fclose($c);
130+ continue;
131+ }
132+
133+ while (($line = fgets($s)) !== false) {
134+ $cmd = rtrim($line, "\r\n");
135+ if ($cmd === 'INFO') {
136+ $i = Tls::info($s);
137+ unset($i['peer_certificates']);
138+ fwrite($s, json_encode($i)."\n");
139+ } else if ($cmd === 'KEYUPDATE') {
140+ Stream::connection($s)->keyUpdate(true);
141+ fwrite($s, "updated\n");
142+ } else if (str_starts_with($cmd, 'BIG ')) {
143+ fwrite($s, str_repeat('x', (int)substr($cmd, 4)));
144+ } else if ($cmd === 'CLOSE') {
145+ break;
146+ } else if ($cmd === 'HELLOREQ') {
147+ $conn->helloRequest();
148+ fwrite($s, "asked\n");
149+ } else if ($cmd === 'GARBAGE') {
150+ fwrite(Stream::connection($s)->socket(), "\x17\x03\x03\x00\x20".random_bytes(32));
151+ } else if ($cmd === 'DROP') {
152+ fclose(Stream::connection($s)->socket());
153+ continue 2;
154+ } else {
155+ fwrite($s, $line);
156+ }
157+ }
158+ fclose($s);
159+}
@@ -41,6 +41,7 @@
4141
4242 use Std\Lib\Tester;
4343 use Std\Lib\Activitypub;
44+use Std\Lib\Crypto\Rsa;
4445 use Site\Controller\BlogPost;
4546
4647 $test = new Tester([
@@ -49,14 +50,13 @@
4950 ]);
5051
5152 $followers = tempnam(sys_get_temp_dir(), 'lbfollow_');
52-$key = function_exists('openssl_pkey_new')
53- ? @openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]) : false;
53+$key = Rsa::generate(2048);
5454
5555 // 外部との通信だけを偽物にする
5656 $make = fn(array $posts = []) => new class($posts, $followers, $key) extends Activitypub {
5757 public array $sent = [];
5858 public array $remote = [];
59- public function __construct(array $posts, private string $file, private mixed $key) {
59+ public function __construct(array $posts, private string $file, private Rsa $key) {
6060 parent::__construct($posts);
6161 }
6262 protected function followersFile(): string { return $this->file; }
@@ -219,10 +219,6 @@
219219 });
220220
221221 $test->it('POSTとGETの署名を作れるはず', function($test) use ($make, $key): void {
222- if ($key === false) {
223- $test->assertTrue(true);
224- return; // OpenSSLの設定が無い環境
225- }
226222 $ap = $make();
227223 $parse = function(array $list): array {
228224 $h = [];
@@ -242,20 +238,16 @@
242238 $test->assertFalse(isset($h['Digest']));
243239 preg_match('/signature="([^"]+)"/', $h['Signature'], $m);
244240 $signed = "(request-target): get /users/x?y=1\nhost: a.example\ndate: {$h['Date']}";
245- $test->assertEquals(1, openssl_verify($signed, base64_decode($m[1]), openssl_pkey_get_details($key)['key'], OPENSSL_ALGO_SHA256));
241+ $test->assertTrue(Rsa::fromPem($key->publicPem())->verify($signed, base64_decode($m[1])));
246242 });
247243
248244 $test->it('受付ボックスの署名を検証するはず', function($test) use ($make, $key, $domain, $remoteActor): void {
249- if ($key === false) {
250- $test->assertTrue(true);
251- return;
252- }
253245 $ap = $make();
254246 $keyId = "{$domain}/ap/actor#main-key";
255247 $actor = "{$domain}/ap/actor";
256248 // 自分の鍵で署名したリクエストを「相手」として検証する
257249 $ap->remote[$actor] = ['id' => $actor, 'publicKey' => ['id' => $keyId, 'owner' => $actor,
258- 'publicKeyPem' => openssl_pkey_get_details($key)['key']]];
250+ 'publicKeyPem' => $key->publicPem()]];
259251
260252 $body = json_encode(['type' => 'Follow', 'actor' => $actor, 'object' => 'x']);
261253 $headers = [];
@@ -41,6 +41,7 @@
4141
4242 use Std\Lib\Tester;
4343 use Std\Lib\Activitypub;
44+use Std\Lib\Crypto\Rsa;
4445 use Std\Lib\Cron;
4546 use Std\Lib\CronTab;
4647
@@ -304,7 +305,7 @@
304305 $this->sent[] = $inboxUrl.' '.basename($activity['object']['id']);
305306 return true;
306307 }
307- public function sign(string $url, string $body, \OpenSSLAsymmetricKey $key): ?array {
308+ public function sign(string $url, string $body, Rsa $key): ?array {
308309 return $this->signRequest($url, $body, $key);
309310 }
310311 };
@@ -437,11 +438,7 @@
437438 });
438439
439440 $test->it('(request-target)を含めて署名するはず', function($test) use ($make): void {
440- $key = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
441- if ($key === false) {
442- $test->assertTrue(true);
443- return; // OpenSSLの設定が無い環境
444- }
441+ $key = Rsa::generate(2048);
445442
446443 $ap = $make([], []);
447444 $body = '{"type":"Create"}';
@@ -459,8 +456,7 @@
459456
460457 preg_match('/signature="([^"]+)"/', $h['Signature'], $m);
461458 $signed = "(request-target): post /users/x/inbox?y=1\nhost: {$h['Host']}\ndate: {$h['Date']}\ndigest: {$h['Digest']}";
462- $pub = openssl_pkey_get_details($key)['key'];
463- $test->assertEquals(1, openssl_verify($signed, base64_decode($m[1]), $pub, OPENSSL_ALGO_SHA256));
459+ $test->assertTrue(Rsa::fromPem($key->publicPem())->verify($signed, base64_decode($m[1])));
464460 });
465461 });
466462
@@ -0,0 +1,398 @@
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\Test;
39+
40+require_once __DIR__.'/../../../autoload.php';
41+
42+use Std\Lib\Tester;
43+use Std\Lib\Crypto\Aes;
44+use Std\Lib\Crypto\Asn1;
45+use Std\Lib\Crypto\ChaCha20;
46+use Std\Lib\Crypto\Ec;
47+use Std\Lib\Crypto\Rsa;
48+use Std\Lib\Crypto\X25519;
49+use Std\Lib\Crypto\X509;
50+
51+$test = new Tester([
52+ 'colorOutput' => true,
53+ 'verboseOutput' => true
54+]);
55+
56+// OpenSSLで作った鍵・証明書・署名(テストの為に保存した物、時間はvectors.jsonのtimeで確認する)
57+$pki = __DIR__.'/Helper/pki/';
58+$vec = json_decode((string)file_get_contents($pki.'vectors.json'), true);
59+// 改行はCRLFかも知れない(Windowsのgit)
60+$pem = fn(string $f): string => str_replace("\r\n", "\n", (string)file_get_contents($pki.$f));
61+
62+$test->describe('ASN.1とPEM', function($test): void {
63+ $test->it('整数を書いて読めるはず(負数、0、上位ビット)', function($test): void {
64+ foreach ([0, 1, 127, 128, 255, 256, -1, -128, -129, PHP_INT_MAX] as $n) {
65+ $der = Asn1::int($n);
66+ $test->assertSame((string)$n, gmp_strval(Asn1::toInt(Asn1::parse($der))), "整数:{$n}");
67+ }
68+ $test->assertSame("\x02\x01\x00", Asn1::int(0));
69+ $test->assertSame("\x02\x02\x00\x80", Asn1::int(128));
70+ $test->assertSame("\x02\x01\xFF", Asn1::int(-1));
71+ $test->assertSame("\x02\x02\xFF\x7F", Asn1::int(-129));
72+ });
73+
74+ $test->it('OIDを書いて読めるはず', function($test): void {
75+ foreach (['1.2.840.113549.1.1.11', '2.5.29.17', '1.3.132.0.35', '2.999.1'] as $oid) {
76+ $test->assertSame($oid, Asn1::toOid(Asn1::parse(Asn1::oid($oid))));
77+ }
78+ $test->assertSame("\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0b", Asn1::oid('1.2.840.113549.1.1.11'));
79+ });
80+
81+ $test->it('長い長さと入れ子を読めるはず', function($test): void {
82+ $big = Asn1::octets(str_repeat('a', 70000));
83+ $der = Asn1::seq(Asn1::int(5), $big, Asn1::seq(Asn1::null(), Asn1::bool(true)));
84+ $c = Asn1::expect(Asn1::parse($der), Asn1::SEQUENCE, 3);
85+ $test->assertSame(70000, strlen($c[1]['value']));
86+ $test->assertSame("\x30\x05\x05\x00\x01\x01\xFF", $c[2]['raw']);
87+ $test->assertCount(2, Asn1::children($c[2]));
88+ });
89+
90+ $test->it('壊れたDERは例外のはず', function($test): void {
91+ foreach (["\x30\x05\x02\x01", "\x30", "\x02\x85\x01\x01\x01\x01\x01", "\x30\x00\x00", "\x1F\x01\x00"] as $bad) {
92+ $test->assertThrows(fn() => Asn1::parse($bad), \UnexpectedValueException::class);
93+ }
94+ });
95+
96+ $test->it('時間を読めるはず(UTCTime、GeneralizedTime)', function($test): void {
97+ $test->assertSame(gmmktime(1, 2, 3, 4, 5, 2026), Asn1::toTime(Asn1::parse(Asn1::time(gmmktime(1, 2, 3, 4, 5, 2026)))));
98+ $test->assertSame("\x17\x0D".'260405010203Z', Asn1::time(gmmktime(1, 2, 3, 4, 5, 2026)));
99+ $test->assertSame("\x18\x0F".'20600101000000Z', Asn1::time(gmmktime(0, 0, 0, 1, 1, 2060)));
100+ $test->assertSame(gmmktime(0, 0, 0, 1, 1, 1999), Asn1::toTime(Asn1::parse("\x17\x0D".'990101000000Z')));
101+ });
102+
103+ $test->it('PEMを書いて読めるはず(複数、暗号化)', function($test): void {
104+ $p = Asn1::pem("\x05\x00", 'TEST').Asn1::pem(str_repeat("\xFF", 100), 'OTHER');
105+ $b = Asn1::unpem("前のゴミ\r\n".$p);
106+ $test->assertCount(2, $b);
107+ $test->assertSame(['label' => 'TEST', 'der' => "\x05\x00"], $b[0]);
108+ $test->assertSame(str_repeat("\xFF", 100), $b[1]['der']);
109+ $enc = "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,00\n\nAAAA\n-----END RSA PRIVATE KEY-----\n";
110+ $test->assertTrue(Asn1::unpem($enc)[0]['encrypted']);
111+ $test->assertThrows(fn() => Rsa::fromPem($enc), \InvalidArgumentException::class);
112+ });
113+});
114+
115+$test->describe('AES・GCM', function($test): void {
116+ $test->it('FIPS-197のブロックを暗号化出来るはず(128、192、256ビット)', function($test): void {
117+ $pt = hex2bin('00112233445566778899aabbccddeeff');
118+ $test->assertSame('69c4e0d86a7b0430d8cdb78070b4c55a', bin2hex((new Aes(hex2bin('000102030405060708090a0b0c0d0e0f')))->encryptBlock($pt)));
119+ $test->assertSame('dda97ca4864cdfe06eaf70a0ec0d7191', bin2hex((new Aes(hex2bin('000102030405060708090a0b0c0d0e0f1011121314151617')))->encryptBlock($pt)));
120+ $test->assertSame('8ea2b7ca516745bfeafc49904b496089', bin2hex((new Aes(hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f')))->encryptBlock($pt)));
121+ });
122+
123+ $test->it('GCMのテストベクトルと同じはず', function($test): void {
124+ $test->assertSame('58e2fccefa7e3061367f1d57a4e7455a', bin2hex((new Aes(str_repeat("\0", 16)))->seal(str_repeat("\0", 12), '')));
125+ $test->assertSame('0388dace60b6a392f328c2b971b2fe78ab6e47d42cec13bdf53a67b21257bddf', bin2hex((new Aes(str_repeat("\0", 16)))->seal(str_repeat("\0", 12), str_repeat("\0", 16))));
126+
127+ $k = hex2bin('feffe9928665731c6d6a8f9467308308');
128+ $iv = hex2bin('cafebabefacedbaddecaf888');
129+ $p = hex2bin('d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255');
130+ $aad = hex2bin('feedfacedeadbeeffeedfacedeadbeefabaddad2');
131+ $test->assertSame('4d5c2af327cd64a62cf35abd2ba6fab4', bin2hex(substr((new Aes($k))->seal($iv, $p), -16)));
132+ $s = (new Aes($k))->seal($iv, substr($p, 0, 60), $aad);
133+ $test->assertSame('42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e0915bc94fbc3221a5db94fae95ae7121a47', bin2hex($s));
134+ $test->assertSame('76fc6ece0f4e1768cddf8853bb2d551b', bin2hex(substr((new Aes($k.$k))->seal($iv, substr($p, 0, 60), $aad), -16)));
135+ });
136+
137+ $test->it('復号出来て、改ざんを見つけるはず', function($test): void {
138+ $a = new Aes(random_bytes(32));
139+ $iv = random_bytes(12);
140+ foreach ([0, 1, 15, 16, 17, 1000] as $len) {
141+ $pt = str_repeat('あ', $len);
142+ $s = $a->seal($iv, $pt, 'aad');
143+ $test->assertSame($pt, $a->open($iv, $s, 'aad'));
144+ $test->assertNull($a->open($iv, $s, 'aaD'));
145+ $bad = $s;
146+ $bad[strlen($bad) - 1] = chr(ord($bad[strlen($bad) - 1]) ^ 1);
147+ $test->assertNull($a->open($iv, $bad, 'aad'));
148+ }
149+ $test->assertNull($a->open($iv, 'short'));
150+ $test->assertThrows(fn() => new Aes('short'), \InvalidArgumentException::class);
151+ });
152+});
153+
154+$test->describe('ChaCha20-Poly1305', function($test): void {
155+ $test->it('RFC 8439のテストベクトルと同じはず', function($test): void {
156+ $k = hex2bin('808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f');
157+ $n = hex2bin('070000004041424344454647');
158+ $aad = hex2bin('50515253c0c1c2c3c4c5c6c7');
159+ $pt = "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
160+ $s = (new ChaCha20($k))->seal($n, $pt, $aad);
161+ $test->assertSame('d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd0600691', bin2hex($s));
162+ $test->assertSame($pt, (new ChaCha20($k))->open($n, $s, $aad));
163+ $test->assertSame('a8061dc1305136c6c22b8baf0c0127a9', bin2hex(ChaCha20::poly1305(hex2bin('85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b'), 'Cryptographic Forum Research Group')));
164+ });
165+
166+ $test->it('Poly1305の繰り上がり(全部0xFF)が正しいはず', function($test): void {
167+ // Pythonの整数で計算した値
168+ $test->assertSame('88a26e9a6b4c629e345775a342ddba74', bin2hex(ChaCha20::poly1305(str_repeat("\xFF", 32), str_repeat("\xFF", 800))));
169+ });
170+
171+ $test->it('改ざんを見つけるはず', function($test): void {
172+ $c = new ChaCha20(random_bytes(32));
173+ $n = random_bytes(12);
174+ $s = $c->seal($n, 'こんにちは', 'x');
175+ $test->assertSame('こんにちは', $c->open($n, $s, 'x'));
176+ $s[0] = chr(ord($s[0]) ^ 0x80);
177+ $test->assertNull($c->open($n, $s, 'x'));
178+ });
179+});
180+
181+$test->describe('X25519', function($test): void {
182+ $test->it('RFC 7748のテストベクトルと同じはず', function($test): void {
183+ $test->assertSame('c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552', bin2hex(X25519::sharedSecret(
184+ hex2bin('a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4'),
185+ hex2bin('e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c'))));
186+ $a = hex2bin('77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a');
187+ $b = hex2bin('5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb');
188+ $test->assertSame('8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a', bin2hex(X25519::publicKey($a)));
189+ $test->assertSame('4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742', bin2hex(X25519::sharedSecret($a, X25519::publicKey($b))));
190+ });
191+
192+ $test->it('小さい位数の点を拒否するはず', function($test): void {
193+ $test->assertThrows(fn() => X25519::sharedSecret(random_bytes(32), str_repeat("\0", 32)), \InvalidArgumentException::class);
194+ });
195+});
196+
197+$test->describe('RSA', function($test) use ($vec, $pem): void {
198+ $test->it('OpenSSLの鍵を読めるはず(PKCS#8、PKCS#1、公開鍵2種類)', function($test) use ($pem): void {
199+ $k = Rsa::fromPem($pem('rsa.key'));
200+ $test->assertTrue($k->isPrivate());
201+ $test->assertSame(2048, $k->bits);
202+ $test->assertSame($pem('rsa.pub'), $k->publicPem());
203+ $test->assertSame($k->publicPem(), Rsa::fromPem($pem('rsa-pkcs1.key'))->publicPem());
204+ $test->assertSame($k->publicPem(), Rsa::fromPem($pem('rsa-pkcs1.pub'))->publicPem());
205+ $test->assertFalse(Rsa::fromPem($pem('rsa.pub'))->isPrivate());
206+ // 書き出した物をまた読める
207+ $test->assertSame($k->publicPem(), Rsa::fromPem($k->toPem())->publicPem());
208+ $test->assertThrows(fn() => Rsa::fromPem($pem('p256.key')), \InvalidArgumentException::class);
209+ });
210+
211+ $test->it('PKCS#1 v1.5の署名がOpenSSLと同じはず', function($test) use ($pem, $vec): void {
212+ $k = Rsa::fromPem($pem('rsa.key'));
213+ $test->assertSame($vec['pkcs1'], bin2hex($k->sign($vec['message'])));
214+ $pub = Rsa::fromPem($pem('rsa.pub'));
215+ $test->assertTrue($pub->verify($vec['message'], hex2bin($vec['pkcs1'])));
216+ $test->assertFalse($pub->verify($vec['message'].'x', hex2bin($vec['pkcs1'])));
217+ $test->assertFalse($pub->verify($vec['message'], hex2bin($vec['pkcs1']), 'sha384'));
218+ $test->assertFalse($pub->verify($vec['message'], 'short'));
219+ });
220+
221+ $test->it('OpenSSLのPSSの署名を確認出来て、自分の署名も確認出来るはず', function($test) use ($pem, $vec): void {
222+ $pub = Rsa::fromPem($pem('rsa.pub'));
223+ $test->assertTrue($pub->verifyPss($vec['message'], hex2bin($vec['pss'])));
224+ $test->assertFalse($pub->verifyPss($vec['message'].'x', hex2bin($vec['pss'])));
225+ // ソルトの長さ(0のソルトはハッシュの長さを期待すると失敗)
226+ $test->assertFalse($pub->verifyPss($vec['message'], hex2bin($vec['pss0'])));
227+ $test->assertTrue($pub->verifyPss($vec['message'], hex2bin($vec['pss0']), 'sha256', 0));
228+ $test->assertTrue($pub->verifyPss($vec['message'], hex2bin($vec['pss0']), 'sha256', -1));
229+ $test->assertFalse($pub->verifyPss($vec['message'], hex2bin($vec['pss']), 'sha256', 0));
230+ $k = Rsa::fromPem($pem('rsa.key'));
231+ foreach (['sha256', 'sha384', 'sha512'] as $h) {
232+ $sig = $k->signPss('データ', $h);
233+ $test->assertTrue($pub->verifyPss('データ', $sig, $h), $h);
234+ $test->assertFalse($pub->verifyPss('データ', $sig, $h === 'sha256' ? 'sha384' : 'sha256'), $h);
235+ }
236+ });
237+
238+ $test->it('OpenSSLのOAEPを復号出来て、暗号化した物を復号出来るはず', function($test) use ($pem, $vec): void {
239+ $k = Rsa::fromPem($pem('rsa.key'));
240+ $test->assertSame('秘密', $k->decrypt(hex2bin($vec['oaep'])));
241+ $ct = Rsa::fromPem($pem('rsa.pub'))->encrypt('もう一つ');
242+ $test->assertSame('もう一つ', $k->decrypt($ct));
243+ $ct[100] = chr(ord($ct[100]) ^ 1);
244+ $test->assertNull($k->decrypt($ct));
245+ $test->assertSame('sha256', $k->decrypt(Rsa::fromPem($pem('rsa.pub'))->encrypt('sha256', 'sha256'), 'sha256'));
246+ $test->assertThrows(fn() => Rsa::fromPem($pem('rsa.pub'))->encrypt(str_repeat('x', 300)), \InvalidArgumentException::class);
247+ });
248+
249+ $test->it('新しい鍵を作れるはず', function($test): void {
250+ $k = Rsa::generate(2048);
251+ $test->assertSame(2048, $k->bits);
252+ $test->assertSame('65537', gmp_strval($k->e));
253+ $copy = Rsa::fromPem($k->toPem());
254+ $test->assertTrue(Rsa::fromPem($k->publicPem())->verify('m', $copy->sign('m')));
255+ $test->assertThrows(fn() => Rsa::generate(512), \InvalidArgumentException::class);
256+ $test->assertThrows(fn() => Rsa::fromPem($k->publicPem())->sign('m'), \LogicException::class);
257+ });
258+});
259+
260+$test->describe('楕円曲線(P-256・P-384・P-521)', function($test) use ($vec, $pem): void {
261+ $test->it('OpenSSLの鍵を読めて、署名を確認出来るはず', function($test) use ($pem, $vec): void {
262+ $k = Ec::fromPem($pem('p256.key'));
263+ $test->assertSame('P-256', $k->curve);
264+ $test->assertSame($pem('p256.pub'), $k->publicPem());
265+ $test->assertSame($k->publicPem(), Ec::fromPem($pem('p256-sec1.key'))->publicPem());
266+ $test->assertTrue(Ec::fromPem($pem('p256.pub'))->verify($vec['message'], hex2bin($vec['es256'])));
267+ // DERでない書き方(rの前に余計な0)は拒否する
268+ $rs = Asn1::children(Asn1::parse(hex2bin($vec['es256'])));
269+ $r = ltrim($rs[0]['value'], "\0");
270+ $loose = Asn1::seq(Asn1::tlv(Asn1::INTEGER, "\0\0".$r), $rs[1]['raw']);
271+ $test->assertFalse(Ec::fromPem($pem('p256.pub'))->verify($vec['message'], $loose));
272+ $test->assertFalse(Ec::fromPem($pem('p256.pub'))->verify($vec['message'], 'not der'));
273+ $test->assertFalse(Ec::fromPem($pem('p256.pub'))->verify($vec['message'].'x', hex2bin($vec['es256'])));
274+ $p384 = Ec::fromPem($pem('p384.pub'));
275+ $test->assertSame('P-384', $p384->curve);
276+ $test->assertTrue($p384->verify($vec['message'], hex2bin($vec['es384']), 'sha384'));
277+ $test->assertFalse($p384->verify($vec['message'], hex2bin($vec['es384']), 'sha256'));
278+ });
279+
280+ $test->it('RFC 6979の決定的な署名と同じはず(A.2.5)', function($test): void {
281+ $der = Asn1::seq(Asn1::int(1), Asn1::octets(hex2bin('C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721')), Asn1::explicit(0, Asn1::oid('1.2.840.10045.3.1.7')));
282+ $rs = Asn1::children(Asn1::parse(Ec::fromDer($der)->sign('sample', 'sha256')));
283+ $test->assertSame('efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716', gmp_strval(Asn1::toInt($rs[0]), 16));
284+ $test->assertSame('f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8', gmp_strval(Asn1::toInt($rs[1]), 16));
285+ });
286+
287+ $test->it('ECDHで同じ秘密になり、署名を確認出来るはず(全部の曲線)', function($test): void {
288+ foreach (['P-256' => 'sha256', 'P-384' => 'sha384', 'P-521' => 'sha512'] as $curve => $h) {
289+ $a = Ec::generate($curve);
290+ $b = Ec::generate($curve);
291+ $test->assertSame($a->ecdh($b->publicPoint()), $b->ecdh($a->publicPoint()), $curve);
292+ $sig = $a->sign('メッセージ', $h);
293+ $pub = Ec::fromPem($a->publicPem());
294+ $test->assertTrue($pub->verify('メッセージ', $sig, $h), $curve);
295+ $test->assertFalse($pub->verify('メッセージ', $b->sign('メッセージ', $h), $h), $curve);
296+ $test->assertSame($a->publicPem(), Ec::fromPem($a->toPem())->publicPem(), $curve);
297+ }
298+ });
299+
300+ $test->it('曲線上にない点を拒否するはず', function($test): void {
301+ $test->assertThrows(fn() => Ec::fromPublic('P-256', "\x04".str_repeat("\x01", 64)), \InvalidArgumentException::class);
302+ $test->assertThrows(fn() => Ec::generate('P-256')->ecdh("\x04".str_repeat("\x02", 64)), \InvalidArgumentException::class);
303+ $test->assertThrows(fn() => Ec::fromPublic('P-256', "\x02".str_repeat("\x01", 32)), \InvalidArgumentException::class);
304+ $test->assertThrows(fn() => Ec::generate('P-192'), \InvalidArgumentException::class);
305+ });
306+});
307+
308+$test->describe('X.509', function($test) use ($vec, $pki): void {
309+ $c = fn(string $f): X509 => X509::fromPem((string)file_get_contents($pki.$f));
310+ $trust = X509::trust([$c('root.pem')]);
311+ $check = function(array $chain, ?string $host, array $opt = []) use ($trust, $vec): string {
312+ try {
313+ X509::verifyChain($chain, $host, $opt + ['trusted' => $trust, 'time' => $vec['time']]);
314+ return 'OK';
315+ } catch (\RuntimeException $e) {
316+ return $e->getMessage();
317+ }
318+ };
319+
320+ $test->it('証明書を読めるはず', function($test) use ($c): void {
321+ $leaf = $c('leaf.pem');
322+ $test->assertSame(3, $leaf->version);
323+ $test->assertSame('CN=www.example.com', $leaf->subject);
324+ $test->assertSame('O=LB Test, CN=LB Intermediate', $leaf->issuer);
325+ $test->assertSame(['dns' => ['www.example.com', '*.api.example.com'], 'ip' => [inet_pton('192.0.2.7')]], $leaf->altNames());
326+ $test->assertSame(['1.3.6.1.5.5.7.3.1'], $leaf->extKeyUsage());
327+ $test->assertSame([false, null], $leaf->basicConstraints());
328+ $test->assertSame([true, 0], $c('int.pem')->basicConstraints());
329+ $test->assertSame([5, 6], $c('int.pem')->keyUsage());
330+ $test->assertTrue($c('root.pem')->isSelfIssued());
331+ $test->assertSame('P-384', $c('int.pem')->publicKey()->curve);
332+ $test->assertSame($leaf->pem(), X509::allFromPem($leaf->pem().$c('int.pem')->pem())[0]->pem());
333+ });
334+
335+ $test->it('正しいチェーンを確認出来るはず(RSA←P-384←RSA)', function($test) use ($c, $check): void {
336+ $chain = [$c('leaf.pem'), $c('int.pem')];
337+ $test->assertSame('OK', $check($chain, 'www.example.com'));
338+ $test->assertSame('OK', $check($chain, 'WWW.Example.COM.'));
339+ $test->assertSame('OK', $check($chain, null));
340+ $test->assertSame('OK', $check([...$chain, $c('root.pem')], 'www.example.com'));
341+ });
342+
343+ $test->it('ホスト名を正しく確認するはず(ワイルドカード、IP)', function($test) use ($c, $check): void {
344+ $chain = [$c('leaf.pem'), $c('int.pem')];
345+ $test->assertSame('OK', $check($chain, 'x.api.example.com'));
346+ $test->assertSame('OK', $check($chain, '192.0.2.7'));
347+ foreach (['api.example.com', 'a.b.api.example.com', 'example.com', 'evil.com', '192.0.2.8', 'www.example.com.evil.com'] as $h) {
348+ $test->assertStringContains('ホスト名が一致しません', $check($chain, $h), $h);
349+ }
350+ });
351+
352+ $test->it('不正なチェーンを拒否するはず', function($test) use ($c, $check, $vec): void {
353+ $leaf = $c('leaf.pem');
354+ $int = $c('int.pem');
355+ $test->assertStringContains('信頼出来ない証明書', $check([$leaf], 'www.example.com'));
356+ $test->assertStringContains('信頼出来ない証明書', $check([$leaf, $int], 'www.example.com', ['trusted' => []]));
357+ $test->assertStringContains('有効期限が切れて', $check([$leaf, $int], 'www.example.com', ['time' => $vec['time'] + 86400 * 100]));
358+ $test->assertStringContains('まだ有効ではありません', $check([$leaf, $int], 'www.example.com', ['time' => $vec['time'] - 86400]));
359+ $test->assertStringContains('サーバー用ではありません', $check([$c('client.pem'), $int], 'www.example.com'));
360+ $test->assertStringContains('未対応の重要な拡張', $check([$c('crit.pem'), $int], 'www.example.com'));
361+ $test->assertStringContains('CAではありません', $check([$leaf, $c('notca.pem')], 'www.example.com'));
362+ $test->assertStringContains('pathLen', $check([$c('deep.pem'), $c('int2.pem'), $int], 'www.example.com'));
363+ $test->assertStringContains('名前の制約', $check([$c('ncbad.pem'), $c('nc.pem')], 'www.example.com'));
364+ $test->assertSame('OK', $check([$c('ncgood.pem'), $c('nc.pem')], 'a.example.org'));
365+ $test->assertStringContains('証明書がありません', $check([], 'x'));
366+ });
367+
368+ $test->it('RSA-PSSで署名された証明書を確認出来るはず', function($test) use ($c, $check): void {
369+ $test->assertSame('OK', $check([$c('pss.pem')], 'pss.example.com'));
370+ });
371+
372+ $test->it('証明書を作れるはず(CA、中間、自己署名)', function($test): void {
373+ $caKey = Rsa::generate(2048);
374+ $ca = X509::issue($caKey, 'テストCA', ['ca' => true, 'days' => 10]);
375+ $key = Ec::generate('P-256');
376+ $leaf = X509::issue($key, 'localhost', ['sans' => ['localhost', '127.0.0.1', '::1'], 'issuer' => [$ca, $caKey]]);
377+ $test->assertSame('CN=テストCA', $leaf->issuer);
378+ $test->assertTrue($leaf->verifySignature($caKey));
379+ $test->assertSame($key->publicPem(), $leaf->publicKey()->publicPem());
380+ foreach (['localhost', '127.0.0.1', '::1', '[::1]'] as $h) {
381+ X509::verifyChain([$leaf], $h, ['trusted' => X509::trust([$ca])]);
382+ }
383+ $test->assertThrows(fn() => X509::verifyChain([$leaf], 'localhost'.'x', ['trusted' => X509::trust([$ca])]), \RuntimeException::class);
384+
385+ $self = X509::issue(Ec::generate('P-384'), 'self.test');
386+ $test->assertThrows(fn() => X509::verifyChain([$self], 'self.test', ['trusted' => []]), \RuntimeException::class);
387+ X509::verifyChain([$self], 'self.test', ['allow_self_signed' => true]);
388+ $test->assertSame('self.test', X509::fromPem($self->pem())->subjectAttr('CN')[0]);
389+ });
390+
391+ $test->it('信頼する証明書のファイルを読めるはず', function($test) use ($pki): void {
392+ $store = X509::store($pki.'root.pem');
393+ $test->assertCount(1, $store);
394+ $test->assertThrows(fn() => X509::store($pki.'nothing.pem'), \RuntimeException::class);
395+ });
396+});
397+
398+$test->printSummary();
@@ -42,6 +42,7 @@
4242 use Std\Lib\Tester;
4343 use Std\Lib\Curl;
4444 use Std\Test\Helper\MockServer;
45+use Std\Test\Helper\TestCert;
4546
4647 $test = new Tester([
4748 'colorOutput' => true,
@@ -251,6 +252,38 @@
251252 });
252253
253254 $srv?->stop();
255+});
256+
257+$test->describe('Curl HTTPS(PHP純正のTLS 1.3、偽物のサーバー)', function($test) use ($local): void {
258+ $cert = TestCert::get('ec');
259+ $srv = CURL_ENABLED ? MockServer::start('http-server.php', [$cert['cert'], $cert['key']]) : null;
260+ if ($srv === null) {
261+ $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効?)');
262+ return;
263+ }
264+ $url = fn(string $path, string $host = '127.0.0.1') => "https://{$host}:{$srv->port}{$path}";
265+
266+ $test->it('HTTPSでリクエスト出来るはず(証明書を確認する)', function($test) use ($local, $url, $cert): void {
267+ foreach (['127.0.0.1', 'localhost'] as $host) {
268+ $curl = $local($url('/echo?x=1', $host))->setCaInfo($cert['ca'])->setMethod('POST')->setPostRaw('{"暗号":true}');
269+ $res = $curl->execute();
270+ $test->assertTrue($res->isSuccess, (string)$res->message);
271+ $req = json_decode($curl->getResponseBody(), true);
272+ $test->assertEquals('/echo?x=1', $req['target']);
273+ $test->assertEquals('{"暗号":true}', $req['body']);
274+ }
275+ });
276+
277+ $test->it('信頼出来ない証明書は拒否して、確認しない設定なら繋がるはず', function($test) use ($local, $url, $cert): void {
278+ $res = $local($url('/echo'))->setCaInfo(TestCert::get('self')['cert'])->execute();
279+ $test->assertFalse($res->isSuccess);
280+ $test->assertStringContains('証明書', (string)$res->message);
281+
282+ $res = $local($url('/echo'))->setCaInfo(TestCert::get('self')['cert'])->setVerifySSL(false)->execute();
283+ $test->assertTrue($res->isSuccess, (string)$res->message);
284+ });
285+
286+ $srv->stop();
254287 });
255288
256289 $test->printSummary();
@@ -44,6 +44,7 @@
4444 use Std\Lib\Db\DbException;
4545 use Std\Lib\Db\Mysql;
4646 use Std\Test\Helper\MockServer;
47+use Std\Test\Helper\TestCert;
4748
4849 $test = new Tester([
4950 'colorOutput' => true,
@@ -64,22 +65,7 @@
6465 return null;
6566 };
6667
67-// TLS・RSA用の自己署名証明書(作れない環境ではnull)
68-$makeCert = function(): ?array {
69- if (!function_exists('openssl_pkey_new')) return null;
70- $key = @openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
71- if (!$key) return null;
72- $csr = @openssl_csr_new(['commonName' => '127.0.0.1'], $key);
73- $crt = $csr ? @openssl_csr_sign($csr, null, $key, 1) : false;
74- if (!$crt || !openssl_x509_export($crt, $certPem) || !openssl_pkey_export($key, $keyPem)) return null;
7568
76- $base = sys_get_temp_dir().'/lbmysql_'.bin2hex(random_bytes(6));
77- file_put_contents("{$base}-cert.pem", $certPem);
78- file_put_contents("{$base}-key.pem", $keyPem);
79- file_put_contents("{$base}-pub.pem", openssl_pkey_get_details($key)['key']);
80- return ['cert' => "{$base}-cert.pem", 'key' => "{$base}-key.pem", 'pub' => "{$base}-pub.pem"];
81-};
82-
8369 $test->describe('Db(ドライバの選択)', function($test) use ($catch): void {
8470 $test->it('DB_ENABLEDが無効なら設定なしで作れないはず', function($test): void {
8571 if (defined('DB_ENABLED') && DB_ENABLED) {
@@ -140,10 +126,11 @@
140126 });
141127 });
142128
143-$cert = $makeCert();
129+// TLS・RSA用の証明書(RSAの鍵、CAが発行した物)
130+$cert = TestCert::get('rsa');
144131
145132 $test->describe('Mysql(偽物のMySQLサーバー)', function($test) use ($catch, $cert): void {
146- $srv = MockServer::start('mysql-server.php', ['mysql_native_password', $cert['cert'] ?? '', $cert['key'] ?? '']);
133+ $srv = MockServer::start('mysql-server.php', ['mysql_native_password', $cert['cert'], $cert['key']]);
147134 if ($srv === null) {
148135 $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効、又はpdo_sqliteが無い?)');
149136 return;
@@ -164,10 +151,6 @@
164151 };
165152 $one = fn(Mysql $my, string $sql) => array_values($my->query($sql)['rows'][0] ?? [])[0] ?? null;
166153 // プリペアドステートメントで1回だけ実行する
167- // 証明書(TLS・RSA)が必要なテスト:作れない環境ではスキップ
168- $itCert = fn(string $desc, callable $fn) => $cert === null
169- ? $test->skip($desc, '証明書を作れない(opensslが無い、又はopenssl.cnfが見えない?)')
170- : $test->it($desc, $fn);
171154 $run = function(Mysql $my, string $sql, array $params = []): array {
172155 $id = $my->prepare($sql);
173156 try {
@@ -243,7 +226,7 @@
243226 $test->assertTrue($my->isConnected());
244227 });
245228
246- $itCert('caching_sha2_password(Auth Switch、RSA、高速認証)で接続出来るはず', function($test) use ($open, $one, $cert, $info, $catch): void {
229+ $test->it('caching_sha2_password(Auth Switch、RSA、高速認証)で接続出来るはず', function($test) use ($open, $one, $cert, $info, $catch): void {
247230 // 1回目:キャッシュに無いので、公開鍵を求めて暗号化する
248231 $my = $open(['username' => 'sha2', 'password' => 'sha2ひみつ']);
249232 $test->assertSame('caching_sha2_password', $my->getServerInfo()['authPlugin']);
@@ -255,13 +238,13 @@
255238 $test->assertSame(1045, $e->getCode());
256239 });
257240
258- $itCert('sha256_password(設定した公開鍵、又はサーバーから)で接続出来るはず', function($test) use ($open, $one, $cert): void {
241+ $test->it('sha256_password(設定した公開鍵、又はサーバーから)で接続出来るはず', function($test) use ($open, $one, $cert): void {
259242 $test->assertSame('sha256_password', $one($open(['username' => 'sha256', 'password' => 'sha256ひみつ']), 'SELECT @@lb_plugin'));
260243 $test->assertSame(1, $one($open(['username' => 'sha256', 'password' => 'sha256ひみつ', 'pubkey' => $cert['pub']]), 'SELECT @@lb_full_auth'));
261244 });
262245
263- $itCert('TLSで接続出来るはず(証明書を確認する)', function($test) use ($open, $one, $cert, $catch): void {
264- $tls = ['cafile' => $cert['cert']];
246+ $test->it('TLSで接続出来るはず(証明書を確認する)', function($test) use ($open, $one, $cert, $catch): void {
247+ $tls = ['cafile' => $cert['ca']];
265248 $my = $open(['tls' => $tls]);
266249 $test->assertSame(1, $one($my, 'SELECT @@lb_tls'));
267250 $test->assertSame('諏訪子', $my->query("SELECT '諏訪子' AS n")['rows'][0]['n']);
@@ -609,9 +592,5 @@
609592
610593 $srv->stop();
611594 });
612-
613-if ($cert !== null) {
614- foreach ($cert as $f) @unlink($f);
615-}
616595
617596 $test->printSummary();
@@ -244,13 +244,10 @@
244244 });
245245
246246 $test->describe('マルチユーザーのFediverse(2つのサーバー)', function($test) use ($use, $mkuser, $seed, $pump, $tmp, $alice, $bob, $carol): void {
247- $openssl = extension_loaded('openssl');
248247 if (!ACTIVITYPUB_MULTIUSER) {
249248 $test->skipAll('マルチユーザーモードが無効(FEDIINFOのisSingleUserがtrue、又はAUTH_ENABLEDが無効)');
250- } else if (!$openssl) {
251- $test->skipAll('openssl拡張が無効(php.iniの「extension=openssl」)');
252249 }
253- if (ACTIVITYPUB_MULTIUSER && $openssl) {
250+ if (ACTIVITYPUB_MULTIUSER) {
254251 $mkuser('A', 'alice', 1);
255252 $mkuser('A', 'carol', 2);
256253 $mkuser('A', 'dave', 3, 0); // BANされた
@@ -561,10 +558,7 @@
561558 $test->it('GETの署名を検証出来るはず', function($test) use ($use, $alice): void {
562559 $use('A');
563560 $key = Actor::privateKey('alice');
564- if ($key === null) {
565- $test->assertTrue(true);
566- return; // OpenSSLの設定が無い環境
567- }
561+ $test->assertNotNull($key);
568562 $ap = new class extends Activitypub {
569563 public array $docs = [];
570564 public function fetchRemoteJson(string $url): ?array { return $this->docs[$url] ?? null; }
@@ -42,6 +42,7 @@
4242 use Std\Lib\Tester;
4343 use Std\Lib\Mailer;
4444 use Std\Test\Helper\MockServer;
45+use Std\Test\Helper\TestCert;
4546
4647 $test = new Tester([
4748 'colorOutput' => true,
@@ -139,6 +140,50 @@
139140 });
140141
141142 $srv?->stop();
143+ @unlink($log);
144+});
145+
146+$test->describe('メーラー(STARTTLS、PHP純正のTLS 1.3)', function($test): void {
147+ $cert = TestCert::get('ec');
148+ $log = tempnam(sys_get_temp_dir(), 'lbsmtp_');
149+ $srv = MockServer::start('smtp-server.php', [$log, $cert['cert'], $cert['key']]);
150+ if ($srv === null) {
151+ $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効?)');
152+ return;
153+ }
154+ $info = fn(array $tls) => ['host' => 'localhost', 'port' => $srv->port, 'user' => 'me@076.moe', 'pass' => 'ひみつ', 'from' => 'me@076.moe', 'tls' => $tls];
155+
156+ $test->it('STARTTLSの後に認証して送れるはず', function($test) use ($info, $log, $cert): void {
157+ file_put_contents($log, '');
158+ $mail = new Mailer($info(['cafile' => $cert['ca']]));
159+ $mail->connect();
160+ try {
161+ $mail->authenticate();
162+ $mail->send('you@example.com', '暗号化', '本文');
163+ } finally {
164+ $mail->disconnect();
165+ }
166+ $out = (string)file_get_contents($log);
167+ $test->assertStringContains("C: STARTTLS\r\n", $out);
168+ $test->assertStringContains('TLS: TLS_', $out);
169+ // パスワードはTLSの後に送る
170+ $test->assertTrue(strpos($out, 'TLS: TLS_') < strpos($out, 'C: '.base64_encode('ひみつ')));
171+ $test->assertStringContains("C: QUIT\r\n", $out);
172+ });
173+
174+ $test->it('信頼出来ない証明書なら認証せずに例外になるはず', function($test) use ($info, $log): void {
175+ file_put_contents($log, '');
176+ $mail = new Mailer($info(['cafile' => TestCert::get('self')['cert']]));
177+ $mail->connect();
178+ try {
179+ $test->assertThrows(fn() => $mail->authenticate(), \Exception::class);
180+ } finally {
181+ $mail->disconnect();
182+ }
183+ $test->assertFalse(str_contains((string)file_get_contents($log), base64_encode('ひみつ')));
184+ });
185+
186+ $srv->stop();
142187 @unlink($log);
143188 });
144189
@@ -0,0 +1,556 @@
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\Test;
39+
40+require_once __DIR__.'/../../../autoload.php';
41+
42+use Std\Lib\Tester;
43+use Std\Lib\Tls;
44+use Std\Lib\Tls\Connection;
45+use Std\Lib\Tls\Stream;
46+use Std\Lib\Tls\TlsException;
47+use Std\Test\Helper\MockServer;
48+use Std\Test\Helper\TestCert;
49+
50+$test = new Tester([
51+ 'colorOutput' => true,
52+ 'verboseOutput' => true
53+]);
54+
55+// 記録したデータを返し、書いたデータを記録するソケット(OpenSSLとの記録を再現する為)
56+final 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();