true, 'verboseOutput' => true ]); // OpenSSLで作った鍵・証明書・署名(テストの為に保存した物、時間はvectors.jsonのtimeで確認する) $pki = __DIR__.'/Helper/pki/'; $vec = json_decode((string)file_get_contents($pki.'vectors.json'), true); // 改行はCRLFかも知れない(Windowsのgit) $pem = fn(string $f): string => str_replace("\r\n", "\n", (string)file_get_contents($pki.$f)); $test->describe('ASN.1とPEM', function($test): void { $test->it('整数を書いて読めるはず(負数、0、上位ビット)', function($test): void { foreach ([0, 1, 127, 128, 255, 256, -1, -128, -129, PHP_INT_MAX] as $n) { $der = Asn1::int($n); $test->assertSame((string)$n, gmp_strval(Asn1::toInt(Asn1::parse($der))), "整数:{$n}"); } $test->assertSame("\x02\x01\x00", Asn1::int(0)); $test->assertSame("\x02\x02\x00\x80", Asn1::int(128)); $test->assertSame("\x02\x01\xFF", Asn1::int(-1)); $test->assertSame("\x02\x02\xFF\x7F", Asn1::int(-129)); }); $test->it('OIDを書いて読めるはず', function($test): void { foreach (['1.2.840.113549.1.1.11', '2.5.29.17', '1.3.132.0.35', '2.999.1'] as $oid) { $test->assertSame($oid, Asn1::toOid(Asn1::parse(Asn1::oid($oid)))); } $test->assertSame("\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0b", Asn1::oid('1.2.840.113549.1.1.11')); }); $test->it('長い長さと入れ子を読めるはず', function($test): void { $big = Asn1::octets(str_repeat('a', 70000)); $der = Asn1::seq(Asn1::int(5), $big, Asn1::seq(Asn1::null(), Asn1::bool(true))); $c = Asn1::expect(Asn1::parse($der), Asn1::SEQUENCE, 3); $test->assertSame(70000, strlen($c[1]['value'])); $test->assertSame("\x30\x05\x05\x00\x01\x01\xFF", $c[2]['raw']); $test->assertCount(2, Asn1::children($c[2])); }); $test->it('壊れたDERは例外のはず', function($test): void { foreach (["\x30\x05\x02\x01", "\x30", "\x02\x85\x01\x01\x01\x01\x01", "\x30\x00\x00", "\x1F\x01\x00"] as $bad) { $test->assertThrows(fn() => Asn1::parse($bad), \UnexpectedValueException::class); } }); $test->it('時間を読めるはず(UTCTime、GeneralizedTime)', function($test): void { $test->assertSame(gmmktime(1, 2, 3, 4, 5, 2026), Asn1::toTime(Asn1::parse(Asn1::time(gmmktime(1, 2, 3, 4, 5, 2026))))); $test->assertSame("\x17\x0D".'260405010203Z', Asn1::time(gmmktime(1, 2, 3, 4, 5, 2026))); $test->assertSame("\x18\x0F".'20600101000000Z', Asn1::time(gmmktime(0, 0, 0, 1, 1, 2060))); $test->assertSame(gmmktime(0, 0, 0, 1, 1, 1999), Asn1::toTime(Asn1::parse("\x17\x0D".'990101000000Z'))); }); $test->it('PEMを書いて読めるはず(複数、暗号化)', function($test): void { $p = Asn1::pem("\x05\x00", 'TEST').Asn1::pem(str_repeat("\xFF", 100), 'OTHER'); $b = Asn1::unpem("前のゴミ\r\n".$p); $test->assertCount(2, $b); $test->assertSame(['label' => 'TEST', 'der' => "\x05\x00"], $b[0]); $test->assertSame(str_repeat("\xFF", 100), $b[1]['der']); $enc = "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,00\n\nAAAA\n-----END RSA PRIVATE KEY-----\n"; $test->assertTrue(Asn1::unpem($enc)[0]['encrypted']); $test->assertThrows(fn() => Rsa::fromPem($enc), \InvalidArgumentException::class); }); }); $test->describe('AES・GCM', function($test): void { $test->it('FIPS-197のブロックを暗号化出来るはず(128、192、256ビット)', function($test): void { $pt = hex2bin('00112233445566778899aabbccddeeff'); $test->assertSame('69c4e0d86a7b0430d8cdb78070b4c55a', bin2hex((new Aes(hex2bin('000102030405060708090a0b0c0d0e0f')))->encryptBlock($pt))); $test->assertSame('dda97ca4864cdfe06eaf70a0ec0d7191', bin2hex((new Aes(hex2bin('000102030405060708090a0b0c0d0e0f1011121314151617')))->encryptBlock($pt))); $test->assertSame('8ea2b7ca516745bfeafc49904b496089', bin2hex((new Aes(hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f')))->encryptBlock($pt))); }); $test->it('GCMのテストベクトルと同じはず', function($test): void { $test->assertSame('58e2fccefa7e3061367f1d57a4e7455a', bin2hex((new Aes(str_repeat("\0", 16)))->seal(str_repeat("\0", 12), ''))); $test->assertSame('0388dace60b6a392f328c2b971b2fe78ab6e47d42cec13bdf53a67b21257bddf', bin2hex((new Aes(str_repeat("\0", 16)))->seal(str_repeat("\0", 12), str_repeat("\0", 16)))); $k = hex2bin('feffe9928665731c6d6a8f9467308308'); $iv = hex2bin('cafebabefacedbaddecaf888'); $p = hex2bin('d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255'); $aad = hex2bin('feedfacedeadbeeffeedfacedeadbeefabaddad2'); $test->assertSame('4d5c2af327cd64a62cf35abd2ba6fab4', bin2hex(substr((new Aes($k))->seal($iv, $p), -16))); $s = (new Aes($k))->seal($iv, substr($p, 0, 60), $aad); $test->assertSame('42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e0915bc94fbc3221a5db94fae95ae7121a47', bin2hex($s)); $test->assertSame('76fc6ece0f4e1768cddf8853bb2d551b', bin2hex(substr((new Aes($k.$k))->seal($iv, substr($p, 0, 60), $aad), -16))); }); $test->it('復号出来て、改ざんを見つけるはず', function($test): void { $a = new Aes(random_bytes(32)); $iv = random_bytes(12); foreach ([0, 1, 15, 16, 17, 1000] as $len) { $pt = str_repeat('あ', $len); $s = $a->seal($iv, $pt, 'aad'); $test->assertSame($pt, $a->open($iv, $s, 'aad')); $test->assertNull($a->open($iv, $s, 'aaD')); $bad = $s; $bad[strlen($bad) - 1] = chr(ord($bad[strlen($bad) - 1]) ^ 1); $test->assertNull($a->open($iv, $bad, 'aad')); } $test->assertNull($a->open($iv, 'short')); $test->assertThrows(fn() => new Aes('short'), \InvalidArgumentException::class); }); }); $test->describe('ChaCha20-Poly1305', function($test): void { $test->it('RFC 8439のテストベクトルと同じはず', function($test): void { $k = hex2bin('808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'); $n = hex2bin('070000004041424344454647'); $aad = hex2bin('50515253c0c1c2c3c4c5c6c7'); $pt = "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."; $s = (new ChaCha20($k))->seal($n, $pt, $aad); $test->assertSame('d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd0600691', bin2hex($s)); $test->assertSame($pt, (new ChaCha20($k))->open($n, $s, $aad)); $test->assertSame('a8061dc1305136c6c22b8baf0c0127a9', bin2hex(ChaCha20::poly1305(hex2bin('85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b'), 'Cryptographic Forum Research Group'))); }); $test->it('Poly1305の繰り上がり(全部0xFF)が正しいはず', function($test): void { // Pythonの整数で計算した値 $test->assertSame('88a26e9a6b4c629e345775a342ddba74', bin2hex(ChaCha20::poly1305(str_repeat("\xFF", 32), str_repeat("\xFF", 800)))); }); $test->it('改ざんを見つけるはず', function($test): void { $c = new ChaCha20(random_bytes(32)); $n = random_bytes(12); $s = $c->seal($n, 'こんにちは', 'x'); $test->assertSame('こんにちは', $c->open($n, $s, 'x')); $s[0] = chr(ord($s[0]) ^ 0x80); $test->assertNull($c->open($n, $s, 'x')); }); }); $test->describe('X25519', function($test): void { $test->it('RFC 7748のテストベクトルと同じはず', function($test): void { $test->assertSame('c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552', bin2hex(X25519::sharedSecret( hex2bin('a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4'), hex2bin('e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c')))); $a = hex2bin('77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a'); $b = hex2bin('5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb'); $test->assertSame('8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a', bin2hex(X25519::publicKey($a))); $test->assertSame('4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742', bin2hex(X25519::sharedSecret($a, X25519::publicKey($b)))); }); $test->it('小さい位数の点を拒否するはず', function($test): void { $test->assertThrows(fn() => X25519::sharedSecret(random_bytes(32), str_repeat("\0", 32)), \InvalidArgumentException::class); }); }); $test->describe('RSA', function($test) use ($vec, $pem): void { $test->it('OpenSSLの鍵を読めるはず(PKCS#8、PKCS#1、公開鍵2種類)', function($test) use ($pem): void { $k = Rsa::fromPem($pem('rsa.key')); $test->assertTrue($k->isPrivate()); $test->assertSame(2048, $k->bits); $test->assertSame($pem('rsa.pub'), $k->publicPem()); $test->assertSame($k->publicPem(), Rsa::fromPem($pem('rsa-pkcs1.key'))->publicPem()); $test->assertSame($k->publicPem(), Rsa::fromPem($pem('rsa-pkcs1.pub'))->publicPem()); $test->assertFalse(Rsa::fromPem($pem('rsa.pub'))->isPrivate()); // 書き出した物をまた読める $test->assertSame($k->publicPem(), Rsa::fromPem($k->toPem())->publicPem()); $test->assertThrows(fn() => Rsa::fromPem($pem('p256.key')), \InvalidArgumentException::class); }); $test->it('PKCS#1 v1.5の署名がOpenSSLと同じはず', function($test) use ($pem, $vec): void { $k = Rsa::fromPem($pem('rsa.key')); $test->assertSame($vec['pkcs1'], bin2hex($k->sign($vec['message']))); $pub = Rsa::fromPem($pem('rsa.pub')); $test->assertTrue($pub->verify($vec['message'], hex2bin($vec['pkcs1']))); $test->assertFalse($pub->verify($vec['message'].'x', hex2bin($vec['pkcs1']))); $test->assertFalse($pub->verify($vec['message'], hex2bin($vec['pkcs1']), 'sha384')); $test->assertFalse($pub->verify($vec['message'], 'short')); }); $test->it('OpenSSLのPSSの署名を確認出来て、自分の署名も確認出来るはず', function($test) use ($pem, $vec): void { $pub = Rsa::fromPem($pem('rsa.pub')); $test->assertTrue($pub->verifyPss($vec['message'], hex2bin($vec['pss']))); $test->assertFalse($pub->verifyPss($vec['message'].'x', hex2bin($vec['pss']))); // ソルトの長さ(0のソルトはハッシュの長さを期待すると失敗) $test->assertFalse($pub->verifyPss($vec['message'], hex2bin($vec['pss0']))); $test->assertTrue($pub->verifyPss($vec['message'], hex2bin($vec['pss0']), 'sha256', 0)); $test->assertTrue($pub->verifyPss($vec['message'], hex2bin($vec['pss0']), 'sha256', -1)); $test->assertFalse($pub->verifyPss($vec['message'], hex2bin($vec['pss']), 'sha256', 0)); $k = Rsa::fromPem($pem('rsa.key')); foreach (['sha256', 'sha384', 'sha512'] as $h) { $sig = $k->signPss('データ', $h); $test->assertTrue($pub->verifyPss('データ', $sig, $h), $h); $test->assertFalse($pub->verifyPss('データ', $sig, $h === 'sha256' ? 'sha384' : 'sha256'), $h); } }); $test->it('OpenSSLのOAEPを復号出来て、暗号化した物を復号出来るはず', function($test) use ($pem, $vec): void { $k = Rsa::fromPem($pem('rsa.key')); $test->assertSame('秘密', $k->decrypt(hex2bin($vec['oaep']))); $ct = Rsa::fromPem($pem('rsa.pub'))->encrypt('もう一つ'); $test->assertSame('もう一つ', $k->decrypt($ct)); $ct[100] = chr(ord($ct[100]) ^ 1); $test->assertNull($k->decrypt($ct)); $test->assertSame('sha256', $k->decrypt(Rsa::fromPem($pem('rsa.pub'))->encrypt('sha256', 'sha256'), 'sha256')); $test->assertThrows(fn() => Rsa::fromPem($pem('rsa.pub'))->encrypt(str_repeat('x', 300)), \InvalidArgumentException::class); }); $test->it('新しい鍵を作れるはず', function($test): void { $k = Rsa::generate(2048); $test->assertSame(2048, $k->bits); $test->assertSame('65537', gmp_strval($k->e)); $copy = Rsa::fromPem($k->toPem()); $test->assertTrue(Rsa::fromPem($k->publicPem())->verify('m', $copy->sign('m'))); $test->assertThrows(fn() => Rsa::generate(512), \InvalidArgumentException::class); $test->assertThrows(fn() => Rsa::fromPem($k->publicPem())->sign('m'), \LogicException::class); }); }); $test->describe('楕円曲線(P-256・P-384・P-521)', function($test) use ($vec, $pem): void { $test->it('OpenSSLの鍵を読めて、署名を確認出来るはず', function($test) use ($pem, $vec): void { $k = Ec::fromPem($pem('p256.key')); $test->assertSame('P-256', $k->curve); $test->assertSame($pem('p256.pub'), $k->publicPem()); $test->assertSame($k->publicPem(), Ec::fromPem($pem('p256-sec1.key'))->publicPem()); $test->assertTrue(Ec::fromPem($pem('p256.pub'))->verify($vec['message'], hex2bin($vec['es256']))); // DERでない書き方(rの前に余計な0)は拒否する $rs = Asn1::children(Asn1::parse(hex2bin($vec['es256']))); $r = ltrim($rs[0]['value'], "\0"); $loose = Asn1::seq(Asn1::tlv(Asn1::INTEGER, "\0\0".$r), $rs[1]['raw']); $test->assertFalse(Ec::fromPem($pem('p256.pub'))->verify($vec['message'], $loose)); $test->assertFalse(Ec::fromPem($pem('p256.pub'))->verify($vec['message'], 'not der')); $test->assertFalse(Ec::fromPem($pem('p256.pub'))->verify($vec['message'].'x', hex2bin($vec['es256']))); $p384 = Ec::fromPem($pem('p384.pub')); $test->assertSame('P-384', $p384->curve); $test->assertTrue($p384->verify($vec['message'], hex2bin($vec['es384']), 'sha384')); $test->assertFalse($p384->verify($vec['message'], hex2bin($vec['es384']), 'sha256')); }); $test->it('RFC 6979の決定的な署名と同じはず(A.2.5)', function($test): void { $der = Asn1::seq(Asn1::int(1), Asn1::octets(hex2bin('C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721')), Asn1::explicit(0, Asn1::oid('1.2.840.10045.3.1.7'))); $rs = Asn1::children(Asn1::parse(Ec::fromDer($der)->sign('sample', 'sha256'))); $test->assertSame('efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716', gmp_strval(Asn1::toInt($rs[0]), 16)); $test->assertSame('f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8', gmp_strval(Asn1::toInt($rs[1]), 16)); }); $test->it('ECDHで同じ秘密になり、署名を確認出来るはず(全部の曲線)', function($test): void { foreach (['P-256' => 'sha256', 'P-384' => 'sha384', 'P-521' => 'sha512'] as $curve => $h) { $a = Ec::generate($curve); $b = Ec::generate($curve); $test->assertSame($a->ecdh($b->publicPoint()), $b->ecdh($a->publicPoint()), $curve); $sig = $a->sign('メッセージ', $h); $pub = Ec::fromPem($a->publicPem()); $test->assertTrue($pub->verify('メッセージ', $sig, $h), $curve); $test->assertFalse($pub->verify('メッセージ', $b->sign('メッセージ', $h), $h), $curve); $test->assertSame($a->publicPem(), Ec::fromPem($a->toPem())->publicPem(), $curve); } }); $test->it('曲線上にない点を拒否するはず', function($test): void { $test->assertThrows(fn() => Ec::fromPublic('P-256', "\x04".str_repeat("\x01", 64)), \InvalidArgumentException::class); $test->assertThrows(fn() => Ec::generate('P-256')->ecdh("\x04".str_repeat("\x02", 64)), \InvalidArgumentException::class); $test->assertThrows(fn() => Ec::fromPublic('P-256', "\x02".str_repeat("\x01", 32)), \InvalidArgumentException::class); $test->assertThrows(fn() => Ec::generate('P-192'), \InvalidArgumentException::class); }); }); $test->describe('X.509', function($test) use ($vec, $pki): void { $c = fn(string $f): X509 => X509::fromPem((string)file_get_contents($pki.$f)); $trust = X509::trust([$c('root.pem')]); $check = function(array $chain, ?string $host, array $opt = []) use ($trust, $vec): string { try { X509::verifyChain($chain, $host, $opt + ['trusted' => $trust, 'time' => $vec['time']]); return 'OK'; } catch (\RuntimeException $e) { return $e->getMessage(); } }; $test->it('証明書を読めるはず', function($test) use ($c): void { $leaf = $c('leaf.pem'); $test->assertSame(3, $leaf->version); $test->assertSame('CN=www.example.com', $leaf->subject); $test->assertSame('O=LB Test, CN=LB Intermediate', $leaf->issuer); $test->assertSame(['dns' => ['www.example.com', '*.api.example.com'], 'ip' => [inet_pton('192.0.2.7')]], $leaf->altNames()); $test->assertSame(['1.3.6.1.5.5.7.3.1'], $leaf->extKeyUsage()); $test->assertSame([false, null], $leaf->basicConstraints()); $test->assertSame([true, 0], $c('int.pem')->basicConstraints()); $test->assertSame([5, 6], $c('int.pem')->keyUsage()); $test->assertTrue($c('root.pem')->isSelfIssued()); $test->assertSame('P-384', $c('int.pem')->publicKey()->curve); $test->assertSame($leaf->pem(), X509::allFromPem($leaf->pem().$c('int.pem')->pem())[0]->pem()); }); $test->it('正しいチェーンを確認出来るはず(RSA←P-384←RSA)', function($test) use ($c, $check): void { $chain = [$c('leaf.pem'), $c('int.pem')]; $test->assertSame('OK', $check($chain, 'www.example.com')); $test->assertSame('OK', $check($chain, 'WWW.Example.COM.')); $test->assertSame('OK', $check($chain, null)); $test->assertSame('OK', $check([...$chain, $c('root.pem')], 'www.example.com')); }); $test->it('ホスト名を正しく確認するはず(ワイルドカード、IP)', function($test) use ($c, $check): void { $chain = [$c('leaf.pem'), $c('int.pem')]; $test->assertSame('OK', $check($chain, 'x.api.example.com')); $test->assertSame('OK', $check($chain, '192.0.2.7')); foreach (['api.example.com', 'a.b.api.example.com', 'example.com', 'evil.com', '192.0.2.8', 'www.example.com.evil.com'] as $h) { $test->assertStringContains('ホスト名が一致しません', $check($chain, $h), $h); } }); $test->it('不正なチェーンを拒否するはず', function($test) use ($c, $check, $vec): void { $leaf = $c('leaf.pem'); $int = $c('int.pem'); $test->assertStringContains('信頼出来ない証明書', $check([$leaf], 'www.example.com')); $test->assertStringContains('信頼出来ない証明書', $check([$leaf, $int], 'www.example.com', ['trusted' => []])); $test->assertStringContains('有効期限が切れて', $check([$leaf, $int], 'www.example.com', ['time' => $vec['time'] + 86400 * 100])); $test->assertStringContains('まだ有効ではありません', $check([$leaf, $int], 'www.example.com', ['time' => $vec['time'] - 86400])); $test->assertStringContains('サーバー用ではありません', $check([$c('client.pem'), $int], 'www.example.com')); $test->assertStringContains('未対応の重要な拡張', $check([$c('crit.pem'), $int], 'www.example.com')); $test->assertStringContains('CAではありません', $check([$leaf, $c('notca.pem')], 'www.example.com')); $test->assertStringContains('pathLen', $check([$c('deep.pem'), $c('int2.pem'), $int], 'www.example.com')); $test->assertStringContains('名前の制約', $check([$c('ncbad.pem'), $c('nc.pem')], 'www.example.com')); $test->assertSame('OK', $check([$c('ncgood.pem'), $c('nc.pem')], 'a.example.org')); $test->assertStringContains('証明書がありません', $check([], 'x')); }); $test->it('RSA-PSSで署名された証明書を確認出来るはず', function($test) use ($c, $check): void { $test->assertSame('OK', $check([$c('pss.pem')], 'pss.example.com')); }); $test->it('証明書を作れるはず(CA、中間、自己署名)', function($test): void { $caKey = Rsa::generate(2048); $ca = X509::issue($caKey, 'テストCA', ['ca' => true, 'days' => 10]); $key = Ec::generate('P-256'); $leaf = X509::issue($key, 'localhost', ['sans' => ['localhost', '127.0.0.1', '::1'], 'issuer' => [$ca, $caKey]]); $test->assertSame('CN=テストCA', $leaf->issuer); $test->assertTrue($leaf->verifySignature($caKey)); $test->assertSame($key->publicPem(), $leaf->publicKey()->publicPem()); foreach (['localhost', '127.0.0.1', '::1', '[::1]'] as $h) { X509::verifyChain([$leaf], $h, ['trusted' => X509::trust([$ca])]); } $test->assertThrows(fn() => X509::verifyChain([$leaf], 'localhost'.'x', ['trusted' => X509::trust([$ca])]), \RuntimeException::class); $self = X509::issue(Ec::generate('P-384'), 'self.test'); $test->assertThrows(fn() => X509::verifyChain([$self], 'self.test', ['trusted' => []]), \RuntimeException::class); X509::verifyChain([$self], 'self.test', ['allow_self_signed' => true]); $test->assertSame('self.test', X509::fromPem($self->pem())->subjectAttr('CN')[0]); }); $test->it('信頼する証明書のファイルを読めるはず', function($test) use ($pki): void { $store = X509::store($pki.'root.pem'); $test->assertCount(1, $store); $test->assertThrows(fn() => X509::store($pki.'nothing.pem'), \RuntimeException::class); }); }); $test->printSummary();