master
ブランチ master
LittleBeast / src / Std / Test / LibCrypto.php
22.9 KB、398行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, and/or sell copies of the Software, subject to the following conditions:
10
11 1. **Origin Attribution**:
12 - You must not misrepresent the origin of the Software; you must not claim
13 you created the original Software.
14 - If the Software is used in a product, you must either:
15 a. Provide clear attribution in the product's documentation, user interface,
16 or other visible areas, **OR**
17 b. Pay the original developers a fee they specify in writing.
18 2. **Usage Restriction**:
19 - The Software, or any derivative works, dependencies, or libraries
20 incorporating it, must not be used for censorship or to suppress freedom of
21 speech, expression, or creativity. Prohibited uses include, but are not
22 limited to:
23 - Censorship of so-called "hate speech", visuals, non-mainstream opinions,
24 ideas, or objective reality.
25 - Tools or systems designed to restrict access to information or
26 artistic works.
27 3. **Notice Preservation**:
28 - This license and the above copyright notice must remain intact in all copies
29 of the source code.
30
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38namespace Std\Test;
39
40require_once __DIR__.'/../../../autoload.php';
41
42use Std\Lib\Tester;
43use Std\Lib\Crypto\Aes;
44use Std\Lib\Crypto\Asn1;
45use Std\Lib\Crypto\ChaCha20;
46use Std\Lib\Crypto\Ec;
47use Std\Lib\Crypto\Rsa;
48use Std\Lib\Crypto\X25519;
49use 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();