From b5e6ba5279f1d7f69eeb09499aeebad516c8053c Mon Sep 17 00:00:00 2001 From: Jan Langer Date: Mon, 17 Feb 2020 18:01:18 +0100 Subject: [PATCH] Verify paths to private and public key [closes #23] [closes #24] Co-authored-by: Jan Machala --- src/Cryptography/CryptographyService.php | 27 +++++- .../InvalidPublicKeyException.php | 19 ++++ .../PrivateKeyFileNotFoundException.php | 8 ++ src/Cryptography/PublicKeyFileException.php | 26 ++++++ .../PublicKeyFileNotFoundException.php | 18 ++++ .../Cryptography/CryptographyServiceTest.php | 88 ++++++++++++++----- 6 files changed, 162 insertions(+), 24 deletions(-) create mode 100644 src/Cryptography/InvalidPublicKeyException.php create mode 100644 src/Cryptography/PrivateKeyFileNotFoundException.php create mode 100644 src/Cryptography/PublicKeyFileException.php create mode 100644 src/Cryptography/PublicKeyFileNotFoundException.php diff --git a/src/Cryptography/CryptographyService.php b/src/Cryptography/CryptographyService.php index 45f7740..a206100 100644 --- a/src/Cryptography/CryptographyService.php +++ b/src/Cryptography/CryptographyService.php @@ -7,6 +7,7 @@ use RobRichards\XMLSecLibs\XMLSecurityDSig; use RobRichards\XMLSecLibs\XMLSecurityKey; use const OPENSSL_ALGO_SHA256; +use function is_file; class CryptographyService { @@ -20,8 +21,17 @@ class CryptographyService /** @var string */ private $publicKeyFile; + /** @var bool */ + private $publicKeyVerified; + public function __construct(string $privateKeyFile, string $publicKeyFile, string $privateKeyPassword = '') { + if (!is_file($privateKeyFile)) { + throw new PrivateKeyFileNotFoundException($privateKeyFile); + } + if (!is_file($publicKeyFile)) { + throw new PublicKeyFileNotFoundException($publicKeyFile); + } $this->privateKeyFile = $privateKeyFile; $this->publicKeyFile = $publicKeyFile; $this->privateKeyPassword = $privateKeyPassword; @@ -69,6 +79,8 @@ public function getBkpCode(string $pkpCode): string public function addWSESignature(string $request): string { + $publicKey = (string) file_get_contents($this->publicKeyFile); + $this->verifyPublicKey($publicKey); $securityKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']); $document = new DOMDocument('1.0'); $document->loadXML($request); @@ -77,10 +89,23 @@ public function addWSESignature(string $request): string $securityKey->loadKey($this->privateKeyFile, true); $wse->addTimestamp(); $wse->signSoapDoc($securityKey, ['algorithm' => XMLSecurityDSig::SHA256]); - $binaryToken = $wse->addBinaryToken(file_get_contents($this->publicKeyFile)); + $binaryToken = $wse->addBinaryToken($publicKey); $wse->attachTokentoSig($binaryToken); return $wse->saveXML(); } + private function verifyPublicKey(string $fileContents): void + { + if ($this->publicKeyVerified) { + return; + } + $publicKeyResource = openssl_get_publickey($fileContents); + if ($publicKeyResource === false) { + throw new InvalidPublicKeyException($this->publicKeyFile, (string) openssl_error_string()); + } + openssl_free_key($publicKeyResource); + $this->publicKeyVerified = true; + } + } diff --git a/src/Cryptography/InvalidPublicKeyException.php b/src/Cryptography/InvalidPublicKeyException.php new file mode 100644 index 0000000..0d4f007 --- /dev/null +++ b/src/Cryptography/InvalidPublicKeyException.php @@ -0,0 +1,19 @@ +publicKeyFile = $publicKeyFile; + } + + public function getPublicKeyFile(): string + { + return $this->publicKeyFile; + } + +} diff --git a/src/Cryptography/PublicKeyFileNotFoundException.php b/src/Cryptography/PublicKeyFileNotFoundException.php new file mode 100644 index 0000000..79333cd --- /dev/null +++ b/src/Cryptography/PublicKeyFileNotFoundException.php @@ -0,0 +1,18 @@ +getReceiptData(); - $crypto = new CryptographyService(__DIR__ . '/../../../cert/EET_CA1_Playground-CZ00000019.key', __DIR__ . '/../../../cert/EET_CA1_Playground-CZ00000019.pub'); + $crypto = $this->createCryptographyServiceWithoutPassword(); $expectedPkp = self::EXPECTED_PKP; $pkpCode = $crypto->getPkpCode($data); @@ -26,11 +30,37 @@ public function testGetCodes(): void self::assertSame(self::EXPECTED_BKP, $crypto->getBkpCode($pkpCode)); } - public function testExceptions(): void + /** + * @dataProvider provideInvalidKeyPaths + * + * @param string $privateKeyPath + * @param string $publicKeyPath + * @param string $expectedExceptionType + * + * @phpstan-param class-string<\Throwable> $expectedExceptionType + */ + public function testInvalidKeyPaths(string $privateKeyPath, string $publicKeyPath, string $expectedExceptionType): void + { + $this->expectException($expectedExceptionType); + new CryptographyService($privateKeyPath, $publicKeyPath); + } + + /** + * @return array[] + */ + public function provideInvalidKeyPaths(): array + { + return [ + [self::PRIVATE_KEY_WITHOUT_PASSWORD_PATH, './foo/path', PublicKeyFileNotFoundException::class], + ['./foo/path', self::PUBLIC_KEY_PATH, PrivateKeyFileNotFoundException::class], + ]; + } + + public function testInvalidPrivateKeyInPkpCalculation(): void { $cryptoService = new CryptographyService( - __DIR__ . '/invalid-certificate.pem', - __DIR__ . '/invalid-certificate.pem' + self::INVALID_KEY_PATH, + self::PUBLIC_KEY_PATH ); try { @@ -38,7 +68,7 @@ public function testExceptions(): void $this->fail(); } catch (PrivateKeyFileException $e) { - $this->assertSame(__DIR__ . '/invalid-certificate.pem', $e->getPrivateKeyFile()); + $this->assertSame(self::INVALID_KEY_PATH, $e->getPrivateKeyFile()); } } @@ -49,10 +79,7 @@ public function testExceptions2(): void { include __DIR__ . '/OpenSslFunctionsMock.php'; - $cryptoService = new CryptographyService( - __DIR__ . '/../../../cert/EET_CA1_Playground-CZ00000019.key', - __DIR__ . '/../../../cert/EET_CA1_Playground-CZ00000019.pub' - ); + $cryptoService = $this->createCryptographyServiceWithoutPassword(); try { $cryptoService->getPkpCode($this->getReceiptData()); @@ -66,10 +93,7 @@ public function testExceptions2(): void public function testWSESignatureWithoutPrivateKeyPassword(): void { $request = $this->getRequestData(); - $crypto = new CryptographyService( - __DIR__ . '/../../../cert/EET_CA1_Playground-CZ00000019.key', - __DIR__ . '/../../../cert/EET_CA1_Playground-CZ00000019.pub' - ); + $crypto = $this->createCryptographyServiceWithoutPassword(); $this->assertNotEmpty($crypto->addWSESignature($request)); } @@ -77,11 +101,7 @@ public function testWSESignatureWithoutPrivateKeyPassword(): void public function testWSESignatureWithPrivateKeyPassword(): void { $request = $this->getRequestData(); - $crypto = new CryptographyService( - __DIR__ . '/../../../cert/EET_CA1_Playground_With_Password-CZ00000019.key', - __DIR__ . '/../../../cert/EET_CA1_Playground-CZ00000019.pub', - 'eet' - ); + $crypto = $this->createCryptographyServiceWithPassword('eet'); $this->assertNotEmpty($crypto->addWSESignature($request)); } @@ -89,17 +109,25 @@ public function testWSESignatureWithPrivateKeyPassword(): void public function testWSESignatureWithInvalidPrivateKeyPassword(): void { $request = $this->getRequestData(); - $crypto = new CryptographyService( - __DIR__ . '/../../../cert/EET_CA1_Playground_With_Password-CZ00000019.key', - __DIR__ . '/../../../cert/EET_CA1_Playground-CZ00000019.pub', - 'invalid' - ); + $crypto = $this->createCryptographyServiceWithPassword('invalid'); $this->expectException(Error::class); $this->expectExceptionMessage('openssl_sign(): supplied key param cannot be coerced into a private key'); $crypto->addWSESignature($request); } + public function testWSESignatureWithInvalidPublicKey(): void + { + $request = $this->getRequestData(); + $crypto = new CryptographyService( + self::PRIVATE_KEY_WITHOUT_PASSWORD_PATH, + self::INVALID_KEY_PATH + ); + + $this->expectException(InvalidPublicKeyException::class); + $crypto->addWSESignature($request); + } + /** * @return mixed[] */ @@ -133,4 +161,18 @@ private function getRequestData(): string return (string) preg_replace($patterns, $replacements, $requestTemplate); } + private function createCryptographyServiceWithoutPassword(): CryptographyService + { + return new CryptographyService(self::PRIVATE_KEY_WITHOUT_PASSWORD_PATH, self::PUBLIC_KEY_PATH); + } + + private function createCryptographyServiceWithPassword(string $password): CryptographyService + { + return new CryptographyService( + self::PRIVATE_KEY_WITH_PASSWORD_PATH, + self::PUBLIC_KEY_PATH, + $password + ); + } + }