Skip to content

Commit 0863a32

Browse files
committed
fix: extract PEM directly from legacy PFX fallback
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent e322c08 commit 0863a32

2 files changed

Lines changed: 66 additions & 7 deletions

File tree

src/Xml/DpsSigner.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,7 @@ private function importPfx(string $pfxContent, string $password, string $cnpj):
6464
$lastError = openssl_error_string() ?: '';
6565

6666
if (str_contains($lastError, self::LEGACY_OPENSSL_ERROR)) {
67-
// Legacy PFX — re-pack via CLI and retry
68-
$pfxContent = $this->repackLegacyPfx($pfxContent, $password);
69-
$ok = openssl_pkcs12_read($pfxContent, $certs, $password);
67+
return $this->extractLegacyPemMaterial($pfxContent, $password, $cnpj);
7068
}
7169

7270
if (!$ok) {
@@ -82,10 +80,12 @@ private function importPfx(string $pfxContent, string $password, string $cnpj):
8280
}
8381

8482
/**
85-
* Re-pack a legacy PFX into a modern one using the OpenSSL CLI.
83+
* Extract private key and leaf certificate from a legacy PFX via OpenSSL CLI.
8684
* The password is passed via environment variable to avoid shell injection.
85+
*
86+
* @return array{string, string} [privateKeyPem, certificatePem]
8787
*/
88-
private function repackLegacyPfx(string $pfxContent, string $password): string
88+
private function extractLegacyPemMaterial(string $pfxContent, string $password, string $cnpj): array
8989
{
9090
$tmpIn = tempnam(sys_get_temp_dir(), 'nfse_in_');
9191
$tmpOut = tempnam(sys_get_temp_dir(), 'nfse_out_');
@@ -100,7 +100,7 @@ private function repackLegacyPfx(string $pfxContent, string $password): string
100100
// Use env var to avoid password in process list (avoids shell injection)
101101
putenv('NFSE_PFX_PASS=' . $password);
102102
$cmd = sprintf(
103-
'openssl pkcs12 -legacy -in %s -passin env:NFSE_PFX_PASS -out %s -passout env:NFSE_PFX_PASS 2>/dev/null',
103+
'openssl pkcs12 -legacy -in %s -passin env:NFSE_PFX_PASS -nodes -out %s 2>/dev/null',
104104
escapeshellarg($tmpIn),
105105
escapeshellarg($tmpOut),
106106
);
@@ -117,7 +117,7 @@ private function repackLegacyPfx(string $pfxContent, string $password): string
117117
throw new PfxImportException('openssl CLI repack produced empty output');
118118
}
119119

120-
return $result;
120+
return $this->extractPemParts($result, $cnpj);
121121
} finally {
122122
putenv('NFSE_PFX_PASS');
123123

@@ -130,6 +130,30 @@ private function repackLegacyPfx(string $pfxContent, string $password): string
130130
}
131131
}
132132

133+
/**
134+
* @return array{string, string} [privateKeyPem, certificatePem]
135+
*/
136+
private function extractPemParts(string $pemBundle, string $cnpj): array
137+
{
138+
$privateKeyMatched = preg_match(
139+
'/-----BEGIN(?: ENCRYPTED)? PRIVATE KEY-----.*?-----END(?: ENCRYPTED)? PRIVATE KEY-----/s',
140+
$pemBundle,
141+
$privateKeyMatches,
142+
) === 1;
143+
144+
$certificateMatched = preg_match(
145+
'/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/s',
146+
$pemBundle,
147+
$certificateMatches,
148+
) === 1;
149+
150+
if (!$privateKeyMatched || !$certificateMatched) {
151+
throw new PfxImportException('Failed to extract PEM material from legacy PFX for CNPJ ' . $cnpj);
152+
}
153+
154+
return [$privateKeyMatches[0], $certificateMatches[0]];
155+
}
156+
133157
/**
134158
* Signs an XML document per ABRASF 2.04 / XML-DSig (RSA-SHA1, enveloped signature).
135159
*

tests/Unit/Xml/DpsSignerTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,39 @@ public function testSignatureElementIsAppendedToDpsRoot(): void
135135

136136
self::assertSame(1, $nodes->length, 'Signature must be a direct child of DPS root');
137137
}
138+
139+
public function testExtractPemPartsReturnsPrivateKeyAndCertificateFromCliBundle(): void
140+
{
141+
$privateKey = openssl_pkey_new([
142+
'private_key_bits' => 2048,
143+
'private_key_type' => OPENSSL_KEYTYPE_RSA,
144+
]);
145+
self::assertNotFalse($privateKey);
146+
147+
$certificateRequest = openssl_csr_new(
148+
['commonName' => $this->testCnpj],
149+
$privateKey,
150+
['digest_alg' => 'sha256'],
151+
);
152+
self::assertNotFalse($certificateRequest);
153+
154+
$certificate = openssl_csr_sign($certificateRequest, null, $privateKey, 1, ['digest_alg' => 'sha256']);
155+
self::assertNotFalse($certificate);
156+
157+
$privateKeyPem = '';
158+
self::assertTrue(openssl_pkey_export($privateKey, $privateKeyPem));
159+
160+
$certificatePem = '';
161+
self::assertTrue(openssl_x509_export($certificate, $certificatePem));
162+
163+
$pemBundle = "Bag Attributes\nlocalKeyID: 01 02 03\n" . $certificatePem . "\n" . $privateKeyPem;
164+
165+
$method = new \ReflectionMethod(DpsSigner::class, 'extractPemParts');
166+
$method->setAccessible(true);
167+
168+
$parts = $method->invoke($this->signer, $pemBundle, $this->testCnpj);
169+
170+
self::assertSame(rtrim($privateKeyPem), $parts[0]);
171+
self::assertSame(rtrim($certificatePem), $parts[1]);
172+
}
138173
}

0 commit comments

Comments
 (0)