Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 73 additions & 9 deletions src/Sign/JSignService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
*/
private function getOpenSslErrors(): array
{
$errors = [];

while (($error = openssl_error_string()) !== false) {
$errors[] = $error;
}

return $errors;
}

/**
* @param list<string> $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,
Expand Down
106 changes: 106 additions & 0 deletions tests/Integration/SignPdfTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand Down
Loading