Skip to content

Commit 04c7630

Browse files
authored
Merge pull request #9 from LibreCodeCoop/fix/legacy-pfx-cli-extraction
fix: extract PEM directly from legacy PFX fallback
2 parents e322c08 + 6be2ab9 commit 04c7630

2 files changed

Lines changed: 70 additions & 13 deletions

File tree

src/Xml/DpsSigner.php

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,28 +64,26 @@ 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

72-
if (!$ok) {
73-
$opensslError = openssl_error_string();
70+
$opensslError = openssl_error_string();
7471

75-
throw new PfxImportException(
76-
'Failed to import PFX for CNPJ ' . $cnpj . ': ' . ($opensslError ?: 'unknown OpenSSL error')
77-
);
78-
}
72+
throw new PfxImportException(
73+
'Failed to import PFX for CNPJ ' . $cnpj . ': ' . ($opensslError ?: 'unknown OpenSSL error')
74+
);
7975
}
8076

8177
return [$certs['pkey'], $certs['cert']];
8278
}
8379

8480
/**
85-
* Re-pack a legacy PFX into a modern one using the OpenSSL CLI.
81+
* Extract private key and leaf certificate from a legacy PFX via OpenSSL CLI.
8682
* The password is passed via environment variable to avoid shell injection.
83+
*
84+
* @return array{string, string} [privateKeyPem, certificatePem]
8785
*/
88-
private function repackLegacyPfx(string $pfxContent, string $password): string
86+
private function extractLegacyPemMaterial(string $pfxContent, string $password, string $cnpj): array
8987
{
9088
$tmpIn = tempnam(sys_get_temp_dir(), 'nfse_in_');
9189
$tmpOut = tempnam(sys_get_temp_dir(), 'nfse_out_');
@@ -100,7 +98,7 @@ private function repackLegacyPfx(string $pfxContent, string $password): string
10098
// Use env var to avoid password in process list (avoids shell injection)
10199
putenv('NFSE_PFX_PASS=' . $password);
102100
$cmd = sprintf(
103-
'openssl pkcs12 -legacy -in %s -passin env:NFSE_PFX_PASS -out %s -passout env:NFSE_PFX_PASS 2>/dev/null',
101+
'openssl pkcs12 -legacy -in %s -passin env:NFSE_PFX_PASS -nodes -out %s 2>/dev/null',
104102
escapeshellarg($tmpIn),
105103
escapeshellarg($tmpOut),
106104
);
@@ -117,7 +115,7 @@ private function repackLegacyPfx(string $pfxContent, string $password): string
117115
throw new PfxImportException('openssl CLI repack produced empty output');
118116
}
119117

120-
return $result;
118+
return $this->extractPemParts($result, $cnpj);
121119
} finally {
122120
putenv('NFSE_PFX_PASS');
123121

@@ -130,6 +128,30 @@ private function repackLegacyPfx(string $pfxContent, string $password): string
130128
}
131129
}
132130

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

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)