From 55640eec873e7f274824c6ed904672ebd75a7691 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Wed, 23 Nov 2022 13:45:58 -0500 Subject: [PATCH] [Composer] Upgrade to phpseclib version 3 - Create custom Key file format for Salmon Magic key - Remove obsolete pemToME and MEtoPem Crypto methods - Remove unused newECKeypair Crypto method - Switch to constant-time Base64 encode/decode in Base64Url Strings methods --- composer.json | 2 +- composer.lock | 26 ++--- src/Module/OStatus/Salmon.php | 9 +- src/Module/PublicRSAKey.php | 13 +-- src/Network/Probe.php | 9 +- src/Protocol/Salmon.php | 18 ++- src/Protocol/Salmon/Format/Magic.php | 77 +++++++++++++ src/Util/Crypto.php | 89 +-------------- src/Util/Strings.php | 31 ++---- tests/datasets/crypto/rsa/salmon-public-magic | 1 + tests/datasets/crypto/rsa/salmon-public-pem | 4 + tests/src/Protocol/SalmonTest.php | 105 ++++++++++++++++++ tests/src/Util/CryptoTest.php | 30 +---- 13 files changed, 241 insertions(+), 173 deletions(-) create mode 100644 src/Protocol/Salmon/Format/Magic.php create mode 100644 tests/datasets/crypto/rsa/salmon-public-magic create mode 100644 tests/datasets/crypto/rsa/salmon-public-pem create mode 100644 tests/src/Protocol/SalmonTest.php diff --git a/composer.json b/composer.json index 34c0f6258..d0fc5619b 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "paragonie/hidden-string": "^1.0", "patrickschur/language-detection": "^5.0.0", "pear/console_table": "^1.3", - "phpseclib/phpseclib": "^2.0", + "phpseclib/phpseclib": "^3.0", "pragmarx/google2fa": "^5.0", "pragmarx/recovery": "^0.2", "psr/container": "^1.0", diff --git a/composer.lock b/composer.lock index 786880fab..1805c9d21 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e8626dc6957dff9cc783daad10cfc26f", + "content-hash": "2e082bac083ca61cc0c22f7055d690bf", "packages": [ { "name": "asika/simple-console", @@ -2952,32 +2952,32 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.38", + "version": "3.0.17", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "b03536539f43a4f9aa33c4f0b2f3a1c752088fcd" + "reference": "dbc2307d5c69aeb22db136c52e91130d7f2ca761" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/b03536539f43a4f9aa33c4f0b2f3a1c752088fcd", - "reference": "b03536539f43a4f9aa33c4f0b2f3a1c752088fcd", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/dbc2307d5c69aeb22db136c52e91130d7f2ca761", + "reference": "dbc2307d5c69aeb22db136c52e91130d7f2ca761", "shasum": "" }, "require": { - "php": ">=5.3.3" + "paragonie/constant_time_encoding": "^1|^2", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" }, "require-dev": { - "phing/phing": "~2.7", - "phpunit/phpunit": "^4.8.35|^5.7|^6.0|^9.4", - "squizlabs/php_codesniffer": "~2.0" + "phpunit/phpunit": "*" }, "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", - "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations.", - "ext-xml": "Install the XML extension to load XML formatted public keys." + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." }, "type": "library", "autoload": { @@ -2985,7 +2985,7 @@ "phpseclib/bootstrap.php" ], "psr-4": { - "phpseclib\\": "phpseclib/" + "phpseclib3\\": "phpseclib/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3054,7 +3054,7 @@ "type": "tidelift" } ], - "time": "2022-09-02T17:04:26+00:00" + "time": "2022-10-24T10:51:50+00:00" }, { "name": "pragmarx/google2fa", diff --git a/src/Module/OStatus/Salmon.php b/src/Module/OStatus/Salmon.php index f674f9300..0d393afcc 100644 --- a/src/Module/OStatus/Salmon.php +++ b/src/Module/OStatus/Salmon.php @@ -142,14 +142,9 @@ class Salmon extends \Friendica\BaseModule throw new HTTPException\BadRequestException(); } - $key_info = explode('.', $key); + $this->logger->info('Key details', ['info' => $key]); - $m = Strings::base64UrlDecode($key_info[1]); - $e = Strings::base64UrlDecode($key_info[2]); - - $this->logger->info('Key details', ['info' => $key_info]); - - $pubkey = Crypto::meToPem($m, $e); + $pubkey = SalmonProtocol::magicKeyToPem($key); // We should have everything we need now. Let's see if it verifies. diff --git a/src/Module/PublicRSAKey.php b/src/Module/PublicRSAKey.php index 523ab174f..fb1eeeb27 100644 --- a/src/Module/PublicRSAKey.php +++ b/src/Module/PublicRSAKey.php @@ -23,11 +23,9 @@ namespace Friendica\Module; use Friendica\BaseModule; use Friendica\Core\System; -use Friendica\DI; use Friendica\Model\User; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Util\Crypto; -use Friendica\Util\Strings; +use Friendica\Protocol\Salmon; /** * prints the public RSA key of a user @@ -47,9 +45,10 @@ class PublicRSAKey extends BaseModule throw new BadRequestException(); } - Crypto::pemToMe($user['spubkey'], $modulus, $exponent); - - $content = 'RSA' . '.' . Strings::base64UrlEncode($modulus, true) . '.' . Strings::base64UrlEncode($exponent, true); - System::httpExit($content, Response::TYPE_BLANK, 'application/magic-public-key'); + System::httpExit( + Salmon::salmonKey($user['spubkey']), + Response::TYPE_BLANK, + 'application/magic-public-key' + ); } } diff --git a/src/Network/Probe.php b/src/Network/Probe.php index fc7113dbd..5c188f252 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -40,6 +40,7 @@ use Friendica\Protocol\ActivityNamespace; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Email; use Friendica\Protocol\Feed; +use Friendica\Protocol\Salmon; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; @@ -1512,12 +1513,10 @@ class Probe $pubkey = $curlResult->getBody(); } - $key = explode('.', $pubkey); + try { + $data['pubkey'] = Salmon::magicKeyToPem($pubkey); + } catch (\Throwable $e) { - if (sizeof($key) >= 3) { - $m = Strings::base64UrlDecode($key[1]); - $e = Strings::base64UrlDecode($key[2]); - $data['pubkey'] = Crypto::meToPem($m, $e); } } } diff --git a/src/Protocol/Salmon.php b/src/Protocol/Salmon.php index b1bdb67e1..7e91b0e3e 100644 --- a/src/Protocol/Salmon.php +++ b/src/Protocol/Salmon.php @@ -25,9 +25,11 @@ use Friendica\Core\Logger; use Friendica\DI; use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\Probe; +use Friendica\Protocol\Salmon\Format\Magic; use Friendica\Util\Crypto; use Friendica\Util\Strings; use Friendica\Util\XML; +use phpseclib3\Crypt\PublicKeyLoader; /** * Salmon Protocol class @@ -243,7 +245,19 @@ class Salmon */ public static function salmonKey(string $pubkey): string { - Crypto::pemToMe($pubkey, $modulus, $exponent); - return 'RSA' . '.' . Strings::base64UrlEncode($modulus, true) . '.' . Strings::base64UrlEncode($exponent, true); + \phpseclib3\Crypt\RSA::addFileFormat(Magic::class); + + return PublicKeyLoader::load($pubkey)->toString('Magic'); + } + + /** + * @param string $magic Magic key format starting with "RSA." + * @return string + */ + public static function magicKeyToPem(string $magic): string + { + \phpseclib3\Crypt\RSA::addFileFormat(Magic::class); + + return (string) PublicKeyLoader::load($magic); } } diff --git a/src/Protocol/Salmon/Format/Magic.php b/src/Protocol/Salmon/Format/Magic.php new file mode 100644 index 000000000..a94ae6bd6 --- /dev/null +++ b/src/Protocol/Salmon/Format/Magic.php @@ -0,0 +1,77 @@ +. + * + */ + +namespace Friendica\Protocol\Salmon\Format; + +use Friendica\Util\Strings; +use phpseclib3\Math\BigInteger; + +/** + * This custom public RSA key format class is meant to be used with the \phpseclib3\Crypto\RSA::addFileFormat method. + * + * It handles Salmon's specific magic key string starting with "RSA." and which MIME type is application/magic-key or + * application/magic-public-key + * + * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#anchor13 + */ +class Magic +{ + public static function load($key, $password = ''): array + { + if (!is_string($key)) { + throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); + } + + $key_info = explode('.', $key); + + if (count($key_info) !== 3) { + throw new \UnexpectedValueException('Key should have three components separated by periods'); + } + + if ($key_info[0] !== 'RSA') { + throw new \UnexpectedValueException('Key first component should be "RSA"'); + } + + if (preg_match('#[+/]#', $key_info[1]) + || preg_match('#[+/]#', $key_info[1]) + ) { + throw new \UnexpectedValueException('Wrong encoding, expecting Base64URLencoding'); + } + + $m = Strings::base64UrlDecode($key_info[1]); + $e = Strings::base64UrlDecode($key_info[2]); + + if (!$m || !$e) { + throw new \UnexpectedValueException('Base64 decoding produced an error'); + } + + return [ + 'modulus' => new BigInteger($m, 256), + 'publicExponent' => new BigInteger($e, 256), + 'isPublicKey' => true, + ]; + } + + public static function savePublicKey(BigInteger $n, BigInteger $e, array $options = []): string + { + return 'RSA.' . Strings::base64UrlEncode($n->toBytes(), true) . '.' . Strings::base64UrlEncode($e->toBytes(), true); + } +} diff --git a/src/Util/Crypto.php b/src/Util/Crypto.php index d983202c0..a5ca5e517 100644 --- a/src/Util/Crypto.php +++ b/src/Util/Crypto.php @@ -21,14 +21,11 @@ namespace Friendica\Util; -use Exception; use Friendica\Core\Hook; use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\DI; -use ParagonIE\ConstantTime\Base64UrlSafe; -use phpseclib\Crypt\RSA; -use phpseclib\Math\BigInteger; +use phpseclib3\Crypt\PublicKeyLoader; /** * Crypto class @@ -66,22 +63,6 @@ class Crypto return openssl_verify($data, $sig, $key, (($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg)); } - /** - /** - * @param string $m modulo - * @param string $e exponent - * @return string - */ - public static function meToPem($m, $e) - { - $rsa = new RSA(); - $rsa->loadKey([ - 'e' => new BigInteger($e, 256), - 'n' => new BigInteger($m, 256) - ]); - return $rsa->getPublicKey(); - } - /** * Transform RSA public keys to standard PEM output * @@ -91,29 +72,7 @@ class Crypto */ public static function rsaToPem(string $key) { - $rsa = new RSA(); - $rsa->setPublicKey($key); - - return $rsa->getPublicKey(RSA::PUBLIC_FORMAT_PKCS8); - } - - /** - * Extracts the modulo and exponent reference from a public PEM key - * - * @param string $key public PEM key - * @param string $modulus (ref) modulo reference - * @param string $exponent (ref) exponent reference - * - * @return void - */ - public static function pemToMe(string $key, &$modulus, &$exponent) - { - $rsa = new RSA(); - $rsa->loadKey($key); - $rsa->setPublicKey(); - - $modulus = $rsa->modulus->toBytes(); - $exponent = $rsa->exponent->toBytes(); + return (string)PublicKeyLoader::load($key); } /** @@ -152,50 +111,6 @@ class Crypto return $response; } - /** - * Create a new elliptic curve key pair - * - * @return array with the elements "prvkey", "pubkey", "vapid-public" and "vapid-private" - */ - public static function newECKeypair() - { - $openssl_options = [ - 'curve_name' => 'prime256v1', - 'private_key_type' => OPENSSL_KEYTYPE_EC - ]; - - $conf = DI::config()->get('system', 'openssl_conf_file'); - if ($conf) { - $openssl_options['config'] = $conf; - } - $result = openssl_pkey_new($openssl_options); - - if (empty($result)) { - throw new Exception('Key creation failed'); - } - - $response = ['prvkey' => '', 'pubkey' => '']; - - // Get private key - openssl_pkey_export($result, $response['prvkey']); - - // Get public key - $pkey = openssl_pkey_get_details($result); - $response['pubkey'] = $pkey['key']; - - // Create VAPID keys - // @see https://github.com/web-push-libs/web-push-php/blob/256a18b2a2411469c94943725fb6eccb9681bd75/src/Utils.php#L60-L62 - $hexString = '04'; - $hexString .= str_pad(bin2hex($pkey['ec']['x']), 64, '0', STR_PAD_LEFT); - $hexString .= str_pad(bin2hex($pkey['ec']['y']), 64, '0', STR_PAD_LEFT); - $response['vapid-public'] = Base64UrlSafe::encode(hex2bin($hexString)); - - // @see https://github.com/web-push-libs/web-push-php/blob/256a18b2a2411469c94943725fb6eccb9681bd75/src/VAPID.php - $response['vapid-private'] = Base64UrlSafe::encode(hex2bin(str_pad(bin2hex($pkey['ec']['d']), 64, '0', STR_PAD_LEFT))); - - return $response; - } - /** * Encrypt a string with 'aes-256-cbc' cipher method. * diff --git a/src/Util/Strings.php b/src/Util/Strings.php index 84a8b6727..565c9bbb0 100644 --- a/src/Util/Strings.php +++ b/src/Util/Strings.php @@ -23,6 +23,7 @@ namespace Friendica\Util; use Friendica\Content\ContactSelector; use Friendica\Core\Logger; +use ParagonIE\ConstantTime\Base64; /** * This class handles string functions @@ -245,16 +246,17 @@ class Strings * @param string $s URL to encode * @param boolean $strip_padding Optional. Default false * @return string Encoded URL + * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params */ public static function base64UrlEncode(string $s, bool $strip_padding = false): string { - $s = strtr(base64_encode($s), '+/', '-_'); - if ($strip_padding) { - $s = str_replace('=', '', $s); + $s = Base64::encodeUnpadded($s); + } else { + $s = Base64::encode($s); } - return $s; + return strtr($s, '+/', '-_'); } /** @@ -263,26 +265,11 @@ class Strings * @param string $s URL to decode * @return string Decoded URL * @throws \Exception + * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params */ public static function base64UrlDecode(string $s): string { - /* - * // Placeholder for new rev of salmon which strips base64 padding. - * // PHP base64_decode handles the un-padded input without requiring this step - * // Uncomment if you find you need it. - * - * $l = strlen($s); - * if (!strpos($s,'=')) { - * $m = $l % 4; - * if ($m == 2) - * $s .= '=='; - * if ($m == 3) - * $s .= '='; - * } - * - */ - - return base64_decode(strtr($s, '-_', '+/')); + return Base64::decode(strtr($s, '-_', '+/')); } /** @@ -515,4 +502,4 @@ class Strings return $text; } -} +} \ No newline at end of file diff --git a/tests/datasets/crypto/rsa/salmon-public-magic b/tests/datasets/crypto/rsa/salmon-public-magic new file mode 100644 index 000000000..2e3823704 --- /dev/null +++ b/tests/datasets/crypto/rsa/salmon-public-magic @@ -0,0 +1 @@ +RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB \ No newline at end of file diff --git a/tests/datasets/crypto/rsa/salmon-public-pem b/tests/datasets/crypto/rsa/salmon-public-pem new file mode 100644 index 000000000..116395985 --- /dev/null +++ b/tests/datasets/crypto/rsa/salmon-public-pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALb7KAWWy1L6lrPtHfAuYVUC4ywo48cm +W9e0ZvP/RQ6gWFIoAUhQ3CQsxtuxTPs7nXcKYCQdLw7jykJ7efZCkbMCAwEAAQ== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/src/Protocol/SalmonTest.php b/tests/src/Protocol/SalmonTest.php new file mode 100644 index 000000000..f0c16ca8c --- /dev/null +++ b/tests/src/Protocol/SalmonTest.php @@ -0,0 +1,105 @@ +. + * + */ + +namespace Friendica\Test\src\Protocol; + +use Friendica\Protocol\Salmon; + +class SalmonTest extends \PHPUnit\Framework\TestCase +{ + public function dataMagic(): array + { + return [ + 'salmon' => [ + 'magic' => file_get_contents(__DIR__ . '/../../datasets/crypto/rsa/salmon-public-magic'), + 'pem' => file_get_contents(__DIR__ . '/../../datasets/crypto/rsa/salmon-public-pem'), + ], + ]; + } + + /** + * @dataProvider dataMagic + * + * @param $magic + * @param $pem + * @return void + * @throws \Exception + */ + public function testSalmonKey($magic, $pem) + { + $this->assertEquals($magic, Salmon::salmonKey($pem)); + } + + /** + * @dataProvider dataMagic + * + * @param $magic + * @param $pem + * @return void + */ + public function testMagicKeyToPem($magic, $pem) + { + $this->assertEquals($pem, Salmon::magicKeyToPem($magic)); + } + + public function dataMagicFailure(): array + { + return [ + 'empty string' => [ + 'magic' => '', + ], + 'Missing algo' => [ + 'magic' => 'tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB', + ], + 'Missing modulus' => [ + 'magic' => 'RSA.AQAB', + ], + 'Missing exponent' => [ + 'magic' => 'RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw', + ], + 'Missing key parts' => [ + 'magic' => 'RSA.', + ], + 'Too many parts' => [ + 'magic' => 'RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB.AQAB', + ], + 'Wrong encoding' => [ + 'magic' => 'RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8/9FDqBYUigBSFDcJCzG27FM+zuddwpgJB0vDuPKQnt59kKRsw.AQAB', + ], + 'Wrong algo' => [ + 'magic' => 'ECDSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB', + ], + ]; + } + + /** + * @dataProvider dataMagicFailure + * + * @param $magic + * @return void + */ + public function testMagicKeyToPemFailure($magic) + { + $this->expectException(\Throwable::class); + + Salmon::magicKeyToPem($magic); + } +} diff --git a/tests/src/Util/CryptoTest.php b/tests/src/Util/CryptoTest.php index 8be5bbcc8..142a1af6e 100644 --- a/tests/src/Util/CryptoTest.php +++ b/tests/src/Util/CryptoTest.php @@ -65,7 +65,7 @@ class CryptoTest extends TestCase self::assertEquals(11111111, $test); } - public function dataRsa() + public function dataRsa(): array { return [ 'diaspora' => [ @@ -92,34 +92,6 @@ class CryptoTest extends TestCase ], ]; } - - /** - * @dataProvider dataPEM - */ - public function testPemToMe(string $key) - { - Crypto::pemToMe($key, $m, $e); - - $expectedRSA = new RSA(); - $expectedRSA->loadKey([ - 'e' => new BigInteger($e, 256), - 'n' => new BigInteger($m, 256) - ]); - - self::assertEquals($expectedRSA->getPublicKey(), $key); - } - - /** - * @dataProvider dataPEM - */ - public function testMeToPem(string $key) - { - Crypto::pemToMe($key, $m, $e); - - $checkKey = Crypto::meToPem($m, $e); - - self::assertEquals($key, $checkKey); - } } /**