| 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(); |