TLSライブラリ
守矢諏訪子 <suwako@076.moe>、2026-11-12 13:56
コミット b5a37f2beedd853f77455345ad2a56c674da1a8d
・親 33021bf
・ファイルを見る
60個のファイル、+6182 -156
変更
README-JP.md
| @@ -222,8 +222,20 @@ | ||
| 222 | 222 | * 最初のクエリで接続します。パラメータ付きのクエリはプリペアドステートメントで実行するので、値をエスケープする必要はありません。 |
| 223 | 223 | * 整数の列は `int`、`FLOAT`・`DOUBLE` は `float`、`NULL` は `null`、その他(`DECIMAL`、日付、`BLOB` 等)は文字列で返します。 |
| 224 | 224 | * エラーは `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` は未対応です。 | |
| 226 | 226 | |
| 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 | + | |
| 227 | 239 | ### コメント |
| 228 | 240 | `config/config.php` で `COMMENTS_ENABLED` を `true` にすると、全部のブログ記事の下にコメント欄が表示されます(書き込みには `AUTH_ENABLED` も必要)。 |
| 229 | 241 | * ログイン中のユーザーはコメント、返信、いいねが出来ます(Markdown:太字、斜体、取り消し線、コード、引用、リスト、リンク)。自分のコメントは削除出来、管理者とスタッフは全部のコメントを削除出来ます。JavaScript は不要です。 |
| @@ -269,7 +281,7 @@ | ||
| 269 | 281 | アップロード出来るのは画像(PNG・JPEG・GIF・WebP、20 MiB まで)と動画(MP4・MKV・WebM、400 MiB まで)で、メタデータ(Exif、XMP、テキスト、MP4 の撮影日時・位置等)を取り除きます。動画は変換しないので、ブラウザで再生出来る形式(H.264 等)でアップロードして下さい。\ |
| 270 | 282 | 大きいファイルを受け付ける様に、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"` もお勧めします。 |
| 271 | 283 | 他のサーバーのアイコンや画像は `/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ビット)は初めて使う時に作ります。 | |
| 273 | 285 | |
| 274 | 286 | ### 二段階認証 |
| 275 | 287 | `config/config.php` で `AUTH_2FA_ENABLED` を `true` にすると、各ユーザーがプロフィールの編集の「二段階認証」から有効に出来ます。`false` なら誰も使えず、有効にしていたユーザーもパスワードだけでログインします。\ |
| @@ -279,5 +291,4 @@ | ||
| 279 | 291 | ## 必要な PHP モジュール |
| 280 | 292 | * php_fileinfo(OpenBSDでデフォルトで有効している) |
| 281 | 293 | * php_gmp |
| 282 | -* php_mbstring(OpenBSDでデフォルトで有効している) | |
| 283 | -* php_openssl(OpenBSDでデフォルトで有効している) | |
| 294 | +* php_mbstring(OpenBSDでデフォルトで有効している) |
変更
README.md
| @@ -220,7 +220,19 @@ | ||
| 220 | 220 | * The connection is opened on the first query. Queries with parameters use prepared statements, so values never need escaping. |
| 221 | 221 | * Integer columns come back as `int`, `FLOAT`/`DOUBLE` as `float`, `NULL` as `null`, and everything else (including `DECIMAL`, dates and `BLOB`) as strings. |
| 222 | 222 | * 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 | +``` | |
| 224 | 236 | |
| 225 | 237 | ### Comments |
| 226 | 238 | 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 @@ | ||
| 40 | 40 | // 静的ファイル(画像・CSS等)の場所。パス(例:「/static」=public/static/)、又は別のサーバーのURL(例:「https://ass.example.com/static」) |
| 41 | 41 | // URLの場合、アップロードされたファイルはpublic/の下の同じパス(例:public/static/)に保存するので、そのサーバーと同期して下さい |
| 42 | 42 | 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', ''); | |
| 43 | 46 | define('SITEINFO', [ |
| 44 | 47 | 'title' => 'Little Beast', |
| 45 | 48 | 'description' => 'PHP Framework', |
| @@ -37,6 +37,7 @@ | ||
| 37 | 37 | */ |
| 38 | 38 | namespace Std\Lib; |
| 39 | 39 | |
| 40 | +use Std\Lib\Crypto\Rsa; | |
| 40 | 41 | use Std\Lib\Curl; |
| 41 | 42 | use Std\Lib\Fedi\Actor; |
| 42 | 43 | use Std\Lib\Fedi\Fedi; |
| @@ -61,7 +62,7 @@ | ||
| 61 | 62 | private array|\Closure $posts = []; |
| 62 | 63 | private string $https; |
| 63 | 64 | /** 秘密鍵のキャッシュ(1回の実行で何度も読まない様に) */ |
| 64 | - private ?\OpenSSLAsymmetricKey $privKey = null; | |
| 65 | + private ?Rsa $privKey = null; | |
| 65 | 66 | |
| 66 | 67 | /** 連合しない(会員限定等の)カテゴリー */ |
| 67 | 68 | private const RESTRICTED = ['memberonly', 'subscriberonly', 'staffonly', 'nintendodevonly']; |
| @@ -630,11 +631,15 @@ | ||
| 630 | 631 | // 鍵の持ち主と鍵のURLは同じサーバーである事 |
| 631 | 632 | if (strtolower((string)parse_url($owner, PHP_URL_HOST)) !== strtolower((string)parse_url($sig['keyId'], PHP_URL_HOST))) return null; |
| 632 | 633 | |
| 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 | + } | |
| 634 | 639 | $raw = base64_decode($sig['signature'], true); |
| 635 | - if (!$pub || $raw === false) return null; | |
| 640 | + if ($raw === false) return null; | |
| 636 | 641 | |
| 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; | |
| 638 | 643 | } |
| 639 | 644 | |
| 640 | 645 | // 機能性メソッド |
| @@ -765,12 +770,12 @@ | ||
| 765 | 770 | * |
| 766 | 771 | * @param string $url 送信先のURL |
| 767 | 772 | * @param string $body 送信する本文(GETは空) |
| 768 | - * @param \OpenSSLAsymmetricKey $priv 秘密鍵 | |
| 773 | + * @param Rsa $priv 秘密鍵 | |
| 769 | 774 | * @param string $method post又はget |
| 770 | 775 | * @param string|null $keyId 鍵のID(既定はブログのアクターの鍵。マルチユーザーではユーザーの鍵) |
| 771 | 776 | * @return array|null 「名前: 値」のヘッダー、署名に失敗した場合はnull |
| 772 | 777 | */ |
| 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 { | |
| 774 | 779 | $p = parse_url($url); |
| 775 | 780 | if (!$p || empty($p['host'])) return null; |
| 776 | 781 | $method = strtolower($method); |
| @@ -791,8 +796,10 @@ | ||
| 791 | 796 | $lines[] = "digest: {$headers['Digest']}"; |
| 792 | 797 | } |
| 793 | 798 | |
| 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()); | |
| 796 | 803 | return null; |
| 797 | 804 | } |
| 798 | 805 | |
| @@ -809,15 +816,19 @@ | ||
| 809 | 816 | * 秘密鍵を読み込む(1回だけ) |
| 810 | 817 | * |
| 811 | 818 | * @param bool $log 失敗をログに書くか |
| 812 | - * @return \OpenSSLAsymmetricKey|null 失敗した場合はnull | |
| 819 | + * @return Rsa|null 失敗した場合はnull | |
| 813 | 820 | */ |
| 814 | - private function privateKey(bool $log = true): ?\OpenSSLAsymmetricKey { | |
| 821 | + private function privateKey(bool $log = true): ?Rsa { | |
| 815 | 822 | if ($this->privKey !== null) return $this->privKey; |
| 816 | 823 | |
| 817 | 824 | $file = FEDIINFO['privkey'] ?? ''; |
| 818 | 825 | $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()) { | |
| 821 | 832 | if ($log) logger(LogType::ActivityPub, "エラー:秘密鍵「{$file}」の読込に失敗"); |
| 822 | 833 | return null; |
| 823 | 834 | } |
| @@ -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 @@ | ||
| 502 | 502 | $errstr = ''; |
| 503 | 503 | |
| 504 | 504 | if ($scheme === 'https') { |
| 505 | + // PHP純正のTLS 1.3・1.2(Std\Lib\Tls) | |
| 505 | 506 | $sslOptions = [ |
| 506 | 507 | 'verify_peer' => $this->verifySSL, |
| 507 | 508 | 'verify_peer_name' => $this->verifySSL, |
| 508 | 509 | 'peer_name' => trim($host, '[]'), |
| 510 | + 'alpn' => ['http/1.1'], | |
| 509 | 511 | ]; |
| 510 | 512 | |
| 511 | 513 | if (!empty($this->caInfoPath) && file_exists($this->caInfoPath)) { |
| 512 | 514 | $sslOptions['cafile'] = $this->caInfoPath; |
| 513 | 515 | } |
| 514 | 516 | |
| 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 | + } | |
| 525 | 524 | } else { |
| 526 | 525 | $socket = @fsockopen($connectHost, $port, $errno, $errstr, $this->timeout); |
| 527 | 526 | } |
| @@ -559,13 +558,16 @@ | ||
| 559 | 558 | $readError = "レスポンスが大き過ぎます(上限:{$this->maxResponseSize}バイト)"; |
| 560 | 559 | break; |
| 561 | 560 | } |
| 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) { | |
| 563 | 562 | $readError = "タイムアウト({$this->timeout}秒)"; |
| 564 | 563 | break; |
| 565 | 564 | } |
| 566 | 565 | if ($chunk === false) break; |
| 567 | 566 | } |
| 568 | 567 | |
| 568 | + // 読み込み中のTLSのエラー(改ざん等) | |
| 569 | + $tlsError = Tls::error($socket); | |
| 570 | + if ($readError === '' && $tlsError !== null) $readError = $tlsError->getMessage(); | |
| 569 | 571 | fclose($socket); |
| 570 | 572 | |
| 571 | 573 | if ($readError !== '') { |
| @@ -37,6 +37,10 @@ | ||
| 37 | 37 | */ |
| 38 | 38 | namespace Std\Lib\Db; |
| 39 | 39 | |
| 40 | +use Std\Lib\Crypto\Rsa; | |
| 41 | +use Std\Lib\Tls; | |
| 42 | +use Std\Lib\Tls\TlsException; | |
| 43 | + | |
| 40 | 44 | /** |
| 41 | 45 | * PHP純正のMySQL(MariaDBも)ドライバ。mysqli・pdo_mysql・socketsのモジュールが不要。 |
| 42 | 46 | * |
| @@ -159,7 +163,7 @@ | ||
| 159 | 163 | * |
| 160 | 164 | * @param array|null $info host、port(3306)、socket(UNIXソケットのパス、指定するとhost・portの代わりに使う)、 |
| 161 | 165 | * username、password、dbname、charset(utf8mb4)、 |
| 162 | - * tls(true=証明書を確認するTLS、又はstream_context_createのsslオプションの配列)、 | |
| 166 | + * tls(true=証明書を確認するTLS、又はStd\Lib\Tlsのオプションの配列:cafile、verify_peer等)、 | |
| 163 | 167 | * timeout(秒、10)、pubkey(caching_sha2_password・sha256_password用のサーバーの公開鍵のPEMファイル、任意)、debug |
| 164 | 168 | * @throws \Exception $infoを省略し、DB_ENABLEDが無効な場合 |
| 165 | 169 | */ |
| @@ -714,31 +718,13 @@ | ||
| 714 | 718 | */ |
| 715 | 719 | private function startTls(): void { |
| 716 | 720 | $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]; | |
| 724 | 722 | |
| 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 | - }); | |
| 733 | 723 | 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()); | |
| 737 | 727 | } |
| 738 | - | |
| 739 | - if ($ok !== true) { | |
| 740 | - $this->fail('TLSの開始に失敗'.($err !== '' ? ": {$err}" : '')); | |
| 741 | - } | |
| 742 | 728 | $this->secure = true; |
| 743 | 729 | } |
| 744 | 730 | |
| @@ -751,10 +737,6 @@ | ||
| 751 | 737 | * @throws DbException |
| 752 | 738 | */ |
| 753 | 739 | private function rsaEncrypt(string $pem, string $scramble): string { |
| 754 | - if (!function_exists('openssl_public_encrypt')) { | |
| 755 | - $this->fail('パスワードの暗号化にはopensslモジュールが必要です(又はTLSで接続して下さい)'); | |
| 756 | - } | |
| 757 | - | |
| 758 | 740 | $pw = $this->password."\0"; |
| 759 | 741 | $xor = ''; |
| 760 | 742 | $len = strlen($scramble); |
| @@ -762,12 +744,12 @@ | ||
| 762 | 744 | $xor .= $pw[$i] ^ $scramble[$i % $len]; |
| 763 | 745 | } |
| 764 | 746 | |
| 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()); | |
| 768 | 752 | } |
| 769 | - | |
| 770 | - return $out; | |
| 771 | 753 | } |
| 772 | 754 | |
| 773 | 755 | /** |
| @@ -1199,7 +1181,7 @@ | ||
| 1199 | 1181 | while (strlen($buf) < $len) { |
| 1200 | 1182 | $chunk = @fread($this->socket, min($len - strlen($buf), 1048576)); |
| 1201 | 1183 | 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); | |
| 1203 | 1185 | if (!$timedOut && $chunk === '' && !feof($this->socket)) continue; |
| 1204 | 1186 | $this->drop(); |
| 1205 | 1187 | $this->fail($timedOut ? "パケットの読み込みがタイムアウト({$this->timeout}秒)" : 'サーバーとの接続が切れました'); |
| @@ -37,6 +37,8 @@ | ||
| 37 | 37 | */ |
| 38 | 38 | namespace Std\Lib\Fedi; |
| 39 | 39 | |
| 40 | +use Std\Lib\Crypto\Rsa; | |
| 41 | + | |
| 40 | 42 | /** |
| 41 | 43 | * ローカルのユーザーのアクター(鍵、プロフィール、WebFinger、コレクション) |
| 42 | 44 | */ |
| @@ -44,30 +46,19 @@ | ||
| 44 | 46 | /** |
| 45 | 47 | * ユーザーの秘密鍵(無ければ作る) |
| 46 | 48 | * |
| 47 | - * @return \OpenSSLAsymmetricKey|null 作れない場合はnull(OpenSSLの設定が無い等) | |
| 49 | + * @return Rsa|null 作れない・読めない場合はnull | |
| 48 | 50 | */ |
| 49 | - public static function privateKey(string $username): ?\OpenSSLAsymmetricKey { | |
| 51 | + public static function privateKey(string $username): ?Rsa { | |
| 50 | 52 | $file = Fedi::userPath($username, 'private.pem'); |
| 51 | 53 | if (!is_file($file)) { |
| 52 | 54 | $lock = fopen($file.'.lock', 'c'); |
| 53 | 55 | if ($lock) flock($lock, LOCK_EX); |
| 54 | 56 | try { |
| 55 | 57 | 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()); | |
| 69 | 60 | @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()); | |
| 71 | 62 | } |
| 72 | 63 | } finally { |
| 73 | 64 | if ($lock) { |
| @@ -76,8 +67,13 @@ | ||
| 76 | 67 | } |
| 77 | 68 | } |
| 78 | 69 | } |
| 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; | |
| 81 | 77 | } |
| 82 | 78 | |
| 83 | 79 | public static function publicKeyPem(string $username): ?string { |
| @@ -39,6 +39,7 @@ | ||
| 39 | 39 | |
| 40 | 40 | use LogType; |
| 41 | 41 | use Result; |
| 42 | +use Std\Lib\Tls\TlsException; | |
| 42 | 43 | |
| 43 | 44 | class Mailer { |
| 44 | 45 | private $socket; |
| @@ -51,6 +52,8 @@ | ||
| 51 | 52 | private ?string $pgpKey = null; |
| 52 | 53 | private ?string $pgpPass = null; |
| 53 | 54 | private int $timeout = 30; |
| 55 | + /** STARTTLSのオプション(Std\Lib\Tlsを参照、例:['cafile' => …]) */ | |
| 56 | + private array $tls = []; | |
| 54 | 57 | |
| 55 | 58 | /** |
| 56 | 59 | * コンストラクタ |
| @@ -68,6 +71,7 @@ | ||
| 68 | 71 | $this->user = $info['user'] ?? null; |
| 69 | 72 | $this->pass = $info['pass'] ?? null; |
| 70 | 73 | $this->from = (string)($info['from'] ?? ''); |
| 74 | + $this->tls = (array)($info['tls'] ?? []); | |
| 71 | 75 | } |
| 72 | 76 | |
| 73 | 77 | /** |
| @@ -163,10 +167,11 @@ | ||
| 163 | 167 | if (strpos($ehloRes, 'STARTTLS') !== false) { |
| 164 | 168 | $this->sendCommand('STARTTLS', 220); |
| 165 | 169 | |
| 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(); | |
| 170 | 175 | logger(LogType::Mailer, $msg); |
| 171 | 176 | throw new \Exception($msg); |
| 172 | 177 | } |
| @@ -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 @@ | ||
| 35 | 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | 37 | */ |
| 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)。 | |
| 40 | 40 | |
| 41 | +require_once __DIR__.'/../../../../autoload.php'; | |
| 42 | + | |
| 43 | +use Std\Lib\Tls; | |
| 44 | + | |
| 41 | 45 | $port = (int)($argv[1] ?? 0); |
| 46 | +$tls = isset($argv[2], $argv[3]) ? ['cert' => $argv[2], 'key' => $argv[3], 'alpn' => ['http/1.1']] : null; | |
| 42 | 47 | $server = @stream_socket_server("tcp://127.0.0.1:{$port}", $errno, $errstr); |
| 43 | 48 | if (!$server) { |
| 44 | 49 | fwrite(STDOUT, "error {$errstr}\n"); |
| @@ -70,6 +75,16 @@ | ||
| 70 | 75 | $c = @stream_socket_accept($server, 60); |
| 71 | 76 | if (!$c) break; // 60秒何も来なければ終了(テストが落ちても残らない様に) |
| 72 | 77 | 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 | + } | |
| 73 | 88 | |
| 74 | 89 | // リクエストの読み込み |
| 75 | 90 | $head = ''; |
| @@ -52,6 +52,11 @@ | ||
| 52 | 52 | // @@lb_tls・@@lb_plugin・@@lb_full_auth・@@lb_caps・@@character_set_client・@@version、 |
| 53 | 53 | // DATABASE()・CONNECTION_ID()・VERSION() |
| 54 | 54 | |
| 55 | +require_once __DIR__.'/../../../../autoload.php'; | |
| 56 | + | |
| 57 | +use Std\Lib\Crypto\Rsa; | |
| 58 | +use Std\Lib\Tls; | |
| 59 | + | |
| 55 | 60 | $port = (int)($argv[1] ?? 0); |
| 56 | 61 | $defaultPlugin = $argv[2] ?? 'mysql_native_password'; |
| 57 | 62 | $certFile = $argv[3] ?? ''; |
| @@ -118,9 +123,9 @@ | ||
| 118 | 123 | public function __construct(private string $defaultPlugin, private string $certFile, private string $keyFile) { |
| 119 | 124 | $this->pdo = new \PDO('sqlite::memory:'); |
| 120 | 125 | $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(); | |
| 124 | 129 | } |
| 125 | 130 | } |
| 126 | 131 | |
| @@ -129,7 +134,7 @@ | ||
| 129 | 134 | // TLSで既に復号されたデータが残っている接続を先に |
| 130 | 135 | $pending = []; |
| 131 | 136 | 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; | |
| 133 | 138 | } |
| 134 | 139 | |
| 135 | 140 | if (empty($pending)) { |
| @@ -220,11 +225,11 @@ | ||
| 220 | 225 | // SSLリクエスト(32バイト) |
| 221 | 226 | if (($caps & self::CLIENT_SSL) && strlen($pkt) === 32) { |
| 222 | 227 | 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 | + } | |
| 228 | 233 | $cn['tls'] = true; |
| 229 | 234 | $pkt = $this->recv($id); |
| 230 | 235 | if ($pkt === null) return false; |
| @@ -357,7 +362,8 @@ | ||
| 357 | 362 | } |
| 358 | 363 | |
| 359 | 364 | 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; | |
| 361 | 367 | $res = ''; |
| 362 | 368 | for ($i = 0; $i < strlen($out); $i++) $res .= $out[$i] ^ $scramble[$i % strlen($scramble)]; |
| 363 | 369 | 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 @@ | ||
| 35 | 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | 37 | */ |
| 38 | -// テスト用の偽物のSMTPサーバー(MockServerが起動する)。使い方:php smtp-server.php ポート 記録ファイル | |
| 39 | -// 受け取ったコマンドとメール本文を記録ファイルに書く。 | |
| 38 | +// テスト用の偽物のSMTPサーバー(MockServerが起動する)。使い方:php smtp-server.php ポート 記録ファイル [証明書 秘密鍵] | |
| 39 | +// 受け取ったコマンドとメール本文を記録ファイルに書く。証明書と秘密鍵を渡すとSTARTTLSに対応する(Std\Lib\Tls)。 | |
| 40 | 40 | |
| 41 | +require_once __DIR__.'/../../../../autoload.php'; | |
| 42 | + | |
| 43 | +use Std\Lib\Tls; | |
| 44 | + | |
| 41 | 45 | $port = (int)($argv[1] ?? 0); |
| 42 | 46 | $log = $argv[2] ?? ''; |
| 47 | +$tls = isset($argv[3], $argv[4]) ? ['cert' => $argv[3], 'key' => $argv[4]] : null; | |
| 43 | 48 | $server = @stream_socket_server("tcp://127.0.0.1:{$port}", $errno, $errstr); |
| 44 | 49 | if (!$server || $log === '') { |
| 45 | 50 | fwrite(STDOUT, "error {$errstr}\n"); |
| @@ -55,6 +60,7 @@ | ||
| 55 | 60 | |
| 56 | 61 | $first = true; |
| 57 | 62 | $quit = false; |
| 63 | + $secure = false; | |
| 58 | 64 | fwrite($c, "220 mock ESMTP\r\n"); |
| 59 | 65 | $auth = 0; |
| 60 | 66 | while (($line = fgets($c)) !== false) { |
| @@ -74,7 +80,18 @@ | ||
| 74 | 80 | |
| 75 | 81 | switch (true) { |
| 76 | 82 | 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); | |
| 78 | 95 | break; |
| 79 | 96 | case str_starts_with(strtoupper($line), 'AUTH LOGIN'): |
| 80 | 97 | $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 @@ | ||
| 41 | 41 | |
| 42 | 42 | use Std\Lib\Tester; |
| 43 | 43 | use Std\Lib\Activitypub; |
| 44 | +use Std\Lib\Crypto\Rsa; | |
| 44 | 45 | use Site\Controller\BlogPost; |
| 45 | 46 | |
| 46 | 47 | $test = new Tester([ |
| @@ -49,14 +50,13 @@ | ||
| 49 | 50 | ]); |
| 50 | 51 | |
| 51 | 52 | $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); | |
| 54 | 54 | |
| 55 | 55 | // 外部との通信だけを偽物にする |
| 56 | 56 | $make = fn(array $posts = []) => new class($posts, $followers, $key) extends Activitypub { |
| 57 | 57 | public array $sent = []; |
| 58 | 58 | 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) { | |
| 60 | 60 | parent::__construct($posts); |
| 61 | 61 | } |
| 62 | 62 | protected function followersFile(): string { return $this->file; } |
| @@ -219,10 +219,6 @@ | ||
| 219 | 219 | }); |
| 220 | 220 | |
| 221 | 221 | $test->it('POSTとGETの署名を作れるはず', function($test) use ($make, $key): void { |
| 222 | - if ($key === false) { | |
| 223 | - $test->assertTrue(true); | |
| 224 | - return; // OpenSSLの設定が無い環境 | |
| 225 | - } | |
| 226 | 222 | $ap = $make(); |
| 227 | 223 | $parse = function(array $list): array { |
| 228 | 224 | $h = []; |
| @@ -242,20 +238,16 @@ | ||
| 242 | 238 | $test->assertFalse(isset($h['Digest'])); |
| 243 | 239 | preg_match('/signature="([^"]+)"/', $h['Signature'], $m); |
| 244 | 240 | $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]))); | |
| 246 | 242 | }); |
| 247 | 243 | |
| 248 | 244 | $test->it('受付ボックスの署名を検証するはず', function($test) use ($make, $key, $domain, $remoteActor): void { |
| 249 | - if ($key === false) { | |
| 250 | - $test->assertTrue(true); | |
| 251 | - return; | |
| 252 | - } | |
| 253 | 245 | $ap = $make(); |
| 254 | 246 | $keyId = "{$domain}/ap/actor#main-key"; |
| 255 | 247 | $actor = "{$domain}/ap/actor"; |
| 256 | 248 | // 自分の鍵で署名したリクエストを「相手」として検証する |
| 257 | 249 | $ap->remote[$actor] = ['id' => $actor, 'publicKey' => ['id' => $keyId, 'owner' => $actor, |
| 258 | - 'publicKeyPem' => openssl_pkey_get_details($key)['key']]]; | |
| 250 | + 'publicKeyPem' => $key->publicPem()]]; | |
| 259 | 251 | |
| 260 | 252 | $body = json_encode(['type' => 'Follow', 'actor' => $actor, 'object' => 'x']); |
| 261 | 253 | $headers = []; |
| @@ -41,6 +41,7 @@ | ||
| 41 | 41 | |
| 42 | 42 | use Std\Lib\Tester; |
| 43 | 43 | use Std\Lib\Activitypub; |
| 44 | +use Std\Lib\Crypto\Rsa; | |
| 44 | 45 | use Std\Lib\Cron; |
| 45 | 46 | use Std\Lib\CronTab; |
| 46 | 47 | |
| @@ -304,7 +305,7 @@ | ||
| 304 | 305 | $this->sent[] = $inboxUrl.' '.basename($activity['object']['id']); |
| 305 | 306 | return true; |
| 306 | 307 | } |
| 307 | - public function sign(string $url, string $body, \OpenSSLAsymmetricKey $key): ?array { | |
| 308 | + public function sign(string $url, string $body, Rsa $key): ?array { | |
| 308 | 309 | return $this->signRequest($url, $body, $key); |
| 309 | 310 | } |
| 310 | 311 | }; |
| @@ -437,11 +438,7 @@ | ||
| 437 | 438 | }); |
| 438 | 439 | |
| 439 | 440 | $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); | |
| 445 | 442 | |
| 446 | 443 | $ap = $make([], []); |
| 447 | 444 | $body = '{"type":"Create"}'; |
| @@ -459,8 +456,7 @@ | ||
| 459 | 456 | |
| 460 | 457 | preg_match('/signature="([^"]+)"/', $h['Signature'], $m); |
| 461 | 458 | $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]))); | |
| 464 | 460 | }); |
| 465 | 461 | }); |
| 466 | 462 |
| @@ -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 @@ | ||
| 42 | 42 | use Std\Lib\Tester; |
| 43 | 43 | use Std\Lib\Curl; |
| 44 | 44 | use Std\Test\Helper\MockServer; |
| 45 | +use Std\Test\Helper\TestCert; | |
| 45 | 46 | |
| 46 | 47 | $test = new Tester([ |
| 47 | 48 | 'colorOutput' => true, |
| @@ -251,6 +252,38 @@ | ||
| 251 | 252 | }); |
| 252 | 253 | |
| 253 | 254 | $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(); | |
| 254 | 287 | }); |
| 255 | 288 | |
| 256 | 289 | $test->printSummary(); |
| @@ -44,6 +44,7 @@ | ||
| 44 | 44 | use Std\Lib\Db\DbException; |
| 45 | 45 | use Std\Lib\Db\Mysql; |
| 46 | 46 | use Std\Test\Helper\MockServer; |
| 47 | +use Std\Test\Helper\TestCert; | |
| 47 | 48 | |
| 48 | 49 | $test = new Tester([ |
| 49 | 50 | 'colorOutput' => true, |
| @@ -64,22 +65,7 @@ | ||
| 64 | 65 | return null; |
| 65 | 66 | }; |
| 66 | 67 | |
| 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; | |
| 75 | 68 | |
| 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 | - | |
| 83 | 69 | $test->describe('Db(ドライバの選択)', function($test) use ($catch): void { |
| 84 | 70 | $test->it('DB_ENABLEDが無効なら設定なしで作れないはず', function($test): void { |
| 85 | 71 | if (defined('DB_ENABLED') && DB_ENABLED) { |
| @@ -140,10 +126,11 @@ | ||
| 140 | 126 | }); |
| 141 | 127 | }); |
| 142 | 128 | |
| 143 | -$cert = $makeCert(); | |
| 129 | +// TLS・RSA用の証明書(RSAの鍵、CAが発行した物) | |
| 130 | +$cert = TestCert::get('rsa'); | |
| 144 | 131 | |
| 145 | 132 | $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']]); | |
| 147 | 134 | if ($srv === null) { |
| 148 | 135 | $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効、又はpdo_sqliteが無い?)'); |
| 149 | 136 | return; |
| @@ -164,10 +151,6 @@ | ||
| 164 | 151 | }; |
| 165 | 152 | $one = fn(Mysql $my, string $sql) => array_values($my->query($sql)['rows'][0] ?? [])[0] ?? null; |
| 166 | 153 | // プリペアドステートメントで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); | |
| 171 | 154 | $run = function(Mysql $my, string $sql, array $params = []): array { |
| 172 | 155 | $id = $my->prepare($sql); |
| 173 | 156 | try { |
| @@ -243,7 +226,7 @@ | ||
| 243 | 226 | $test->assertTrue($my->isConnected()); |
| 244 | 227 | }); |
| 245 | 228 | |
| 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 { | |
| 247 | 230 | // 1回目:キャッシュに無いので、公開鍵を求めて暗号化する |
| 248 | 231 | $my = $open(['username' => 'sha2', 'password' => 'sha2ひみつ']); |
| 249 | 232 | $test->assertSame('caching_sha2_password', $my->getServerInfo()['authPlugin']); |
| @@ -255,13 +238,13 @@ | ||
| 255 | 238 | $test->assertSame(1045, $e->getCode()); |
| 256 | 239 | }); |
| 257 | 240 | |
| 258 | - $itCert('sha256_password(設定した公開鍵、又はサーバーから)で接続出来るはず', function($test) use ($open, $one, $cert): void { | |
| 241 | + $test->it('sha256_password(設定した公開鍵、又はサーバーから)で接続出来るはず', function($test) use ($open, $one, $cert): void { | |
| 259 | 242 | $test->assertSame('sha256_password', $one($open(['username' => 'sha256', 'password' => 'sha256ひみつ']), 'SELECT @@lb_plugin')); |
| 260 | 243 | $test->assertSame(1, $one($open(['username' => 'sha256', 'password' => 'sha256ひみつ', 'pubkey' => $cert['pub']]), 'SELECT @@lb_full_auth')); |
| 261 | 244 | }); |
| 262 | 245 | |
| 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']]; | |
| 265 | 248 | $my = $open(['tls' => $tls]); |
| 266 | 249 | $test->assertSame(1, $one($my, 'SELECT @@lb_tls')); |
| 267 | 250 | $test->assertSame('諏訪子', $my->query("SELECT '諏訪子' AS n")['rows'][0]['n']); |
| @@ -609,9 +592,5 @@ | ||
| 609 | 592 | |
| 610 | 593 | $srv->stop(); |
| 611 | 594 | }); |
| 612 | - | |
| 613 | -if ($cert !== null) { | |
| 614 | - foreach ($cert as $f) @unlink($f); | |
| 615 | -} | |
| 616 | 595 | |
| 617 | 596 | $test->printSummary(); |
| @@ -244,13 +244,10 @@ | ||
| 244 | 244 | }); |
| 245 | 245 | |
| 246 | 246 | $test->describe('マルチユーザーのFediverse(2つのサーバー)', function($test) use ($use, $mkuser, $seed, $pump, $tmp, $alice, $bob, $carol): void { |
| 247 | - $openssl = extension_loaded('openssl'); | |
| 248 | 247 | if (!ACTIVITYPUB_MULTIUSER) { |
| 249 | 248 | $test->skipAll('マルチユーザーモードが無効(FEDIINFOのisSingleUserがtrue、又はAUTH_ENABLEDが無効)'); |
| 250 | - } else if (!$openssl) { | |
| 251 | - $test->skipAll('openssl拡張が無効(php.iniの「extension=openssl」)'); | |
| 252 | 249 | } |
| 253 | - if (ACTIVITYPUB_MULTIUSER && $openssl) { | |
| 250 | + if (ACTIVITYPUB_MULTIUSER) { | |
| 254 | 251 | $mkuser('A', 'alice', 1); |
| 255 | 252 | $mkuser('A', 'carol', 2); |
| 256 | 253 | $mkuser('A', 'dave', 3, 0); // BANされた |
| @@ -561,10 +558,7 @@ | ||
| 561 | 558 | $test->it('GETの署名を検証出来るはず', function($test) use ($use, $alice): void { |
| 562 | 559 | $use('A'); |
| 563 | 560 | $key = Actor::privateKey('alice'); |
| 564 | - if ($key === null) { | |
| 565 | - $test->assertTrue(true); | |
| 566 | - return; // OpenSSLの設定が無い環境 | |
| 567 | - } | |
| 561 | + $test->assertNotNull($key); | |
| 568 | 562 | $ap = new class extends Activitypub { |
| 569 | 563 | public array $docs = []; |
| 570 | 564 | public function fetchRemoteJson(string $url): ?array { return $this->docs[$url] ?? null; } |
| @@ -42,6 +42,7 @@ | ||
| 42 | 42 | use Std\Lib\Tester; |
| 43 | 43 | use Std\Lib\Mailer; |
| 44 | 44 | use Std\Test\Helper\MockServer; |
| 45 | +use Std\Test\Helper\TestCert; | |
| 45 | 46 | |
| 46 | 47 | $test = new Tester([ |
| 47 | 48 | 'colorOutput' => true, |
| @@ -139,6 +140,50 @@ | ||
| 139 | 140 | }); |
| 140 | 141 | |
| 141 | 142 | $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(); | |
| 142 | 187 | @unlink($log); |
| 143 | 188 | }); |
| 144 | 189 |
| @@ -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(); |