diff --git a/src/Sign/JSignService.php b/src/Sign/JSignService.php index 5c37002..ea7c304 100644 --- a/src/Sign/JSignService.php +++ b/src/Sign/JSignService.php @@ -392,42 +392,106 @@ private function pkcs12Read(JSignParam $params): array { $certificate = $params->getCertificate(); $password = $params->getPassword(); + + $this->clearOpenSslErrors(); + if (openssl_pkcs12_read($certificate, $certInfo, $password)) { $this->repackCertificateIfPasswordIsUnicode($params, $certInfo['cert'], $certInfo['pkey']); return $certInfo; } - $msg = openssl_error_string(); - if ($msg === 'error:0308010C:digital envelope routines::unsupported') { + + $errors = $this->getOpenSslErrors(); + if ($this->hasUnsupportedLegacyAlgorithmError($errors)) { $opensslVersion = exec('openssl version'); if ($opensslVersion === false) { return []; } + $tempPassword = tempnam(sys_get_temp_dir(), 'pfx'); $tempEncriptedOriginal = tempnam(sys_get_temp_dir(), 'original'); $tempEncriptedRepacked = tempnam(sys_get_temp_dir(), 'repacked'); $tempDecrypted = tempnam(sys_get_temp_dir(), 'decripted'); + if ($tempDecrypted === false || $tempPassword === false || $tempEncriptedOriginal === false || $tempEncriptedRepacked === false) { return []; } + file_put_contents($tempPassword, $password); file_put_contents($tempEncriptedOriginal, $certificate); - $this->safeExec($tempPassword, $tempEncriptedOriginal, $tempDecrypted, $tempEncriptedRepacked); + + $this->safeExec( + $tempPassword, + $tempEncriptedOriginal, + $tempDecrypted, + $tempEncriptedRepacked + ); + $certificateRepacked = file_get_contents($tempEncriptedRepacked); - if ($certificateRepacked === false) { - return []; - } - $params->setCertificate($certificateRepacked); + unlink($tempPassword); unlink($tempEncriptedOriginal); unlink($tempEncriptedRepacked); unlink($tempDecrypted); - openssl_pkcs12_read($certificateRepacked, $certInfo, $password); - $this->repackCertificateIfPasswordIsUnicode($params, $certInfo['cert'], $certInfo['pkey']); + + if ($certificateRepacked === false) { + return []; + } + + $this->clearOpenSslErrors(); + + if (!openssl_pkcs12_read($certificateRepacked, $certInfo, $password)) { + $this->getOpenSslErrors(); + return []; + } + + $params->setCertificate($certificateRepacked); + + $this->repackCertificateIfPasswordIsUnicode( + $params, + $certInfo['cert'], + $certInfo['pkey'] + ); + return $certInfo; } + return []; } + private function clearOpenSslErrors(): void + { + while (openssl_error_string() !== false) { + } + } + + /** + * @return list + */ + private function getOpenSslErrors(): array + { + $errors = []; + + while (($error = openssl_error_string()) !== false) { + $errors[] = $error; + } + + return $errors; + } + + /** + * @param list $errors + */ + private function hasUnsupportedLegacyAlgorithmError(array $errors): bool + { + foreach ($errors as $error) { + if (str_contains($error, 'digital envelope routines::unsupported')) { + return true; + } + } + + return false; + } + private function safeExec( string $tempPassword, string $tempEncriptedOriginal, diff --git a/tests/Integration/SignPdfTest.php b/tests/Integration/SignPdfTest.php index f3f6851..cd0e872 100644 --- a/tests/Integration/SignPdfTest.php +++ b/tests/Integration/SignPdfTest.php @@ -4,6 +4,7 @@ use Jeidison\JSignPDF\JSignPDF; use Jeidison\JSignPDF\Sign\JSignParam; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; @@ -29,6 +30,111 @@ private function params(): JSignParam return $params; } + private function legacyCertificateParams(): JSignParam + { + $tempDir = sys_get_temp_dir() . '/jsignpdf-legacy-' . bin2hex(random_bytes(8)); + + if (! mkdir($tempDir, 0700, true) && ! is_dir($tempDir)) { + $this->fail('Could not create temporary directory.'); + } + + $key = $tempDir . '/key.pem'; + $cert = $tempDir . '/cert.pem'; + $pkcs12 = $tempDir . '/certificate.p12'; + + try { + exec( + sprintf( + 'openssl req -x509 -newkey rsa:2048 -nodes -keyout %s -out %s' + . ' -subj %s -days 1 2>&1', + escapeshellarg($key), + escapeshellarg($cert), + escapeshellarg('/CN=JSignPdf Legacy Test') + ), + $output, + $exitCode + ); + + $this->assertSame( + 0, + $exitCode, + 'Could not generate test certificate: ' . implode(PHP_EOL, $output) + ); + + $output = []; + + exec( + sprintf( + 'openssl pkcs12 -export -legacy -inkey %s -in %s -out %s' + . ' -passout %s 2>&1', + escapeshellarg($key), + escapeshellarg($cert), + escapeshellarg($pkcs12), + escapeshellarg('pass:' . self::PASSWORD) + ), + $output, + $exitCode + ); + + if ($exitCode !== 0) { + $this->markTestSkipped( + 'The installed OpenSSL does not support legacy PKCS#12 generation.' + ); + } + + $certificate = file_get_contents($pkcs12); + $this->assertNotFalse($certificate); + } finally { + foreach ([$key, $cert, $pkcs12] as $file) { + if (is_file($file)) { + unlink($file); + } + } + + if (is_dir($tempDir)) { + rmdir($tempDir); + } + } + + $params = JSignParam::instance(); + $params->setCertificate($certificate); + $params->setPdf(file_get_contents(__DIR__ . '/../resources/pdf-test.pdf')); + $params->setPassword(self::PASSWORD); + + return $params; + } + + #[DataProvider('opensslErrorQueueProvider')] + public function testSignWithLegacyCertificateIgnoresPreviousOpenSslErrors( + string $errorSource + ): void { + while (openssl_error_string() !== false) { + } + + match ($errorSource) { + 'x509' => @openssl_x509_read('not-a-certificate'), + 'private-key' => @openssl_pkey_get_private('not-a-private-key'), + 'pkcs12' => (function (): void { + $certificates = []; + @openssl_pkcs12_read('not-a-pkcs12', $certificates, 'wrong-password'); + })(), + }; + + $signed = JSignPDF::instance($this->legacyCertificateParams())->sign(); + + $this->assertStringStartsWith('%PDF-', $signed); + $this->assertStringContainsString('/ByteRange', $signed); + } + + public static function opensslErrorQueueProvider(): array + { + return [ + 'previous X509 error' => ['x509'], + 'previous private key error' => ['private-key'], + 'previous PKCS12 error' => ['pkcs12'], + ]; + } + public function testGetVersionReturnsTheInstalledJSignPdf(): void { $version = JSignPDF::instance($this->params())->getVersion();