From cac2bbace21203a3fc7997c94fb7dee730d7c05e Mon Sep 17 00:00:00 2001 From: jalesc Date: Thu, 23 Apr 2026 09:59:19 -0300 Subject: [PATCH] =?UTF-8?q?Certificado=20digital=20em=20mem=C3=B3ria?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../emitir_com_certificado_em_memoria.php | 94 +++++++++++++++++++ src/Http/Client/AdnClient.php | 23 ++++- src/Http/Client/CncClient.php | 23 ++++- src/Http/Client/SefinClient.php | 23 ++++- src/Http/NfseContext.php | 9 +- src/Service/ContribuinteService.php | 13 ++- src/Signer/Certificate.php | 10 ++ tests/Unit/Signer/CertificateTest.php | 34 +++++++ 8 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 examples/contribuinte/emitir_com_certificado_em_memoria.php diff --git a/examples/contribuinte/emitir_com_certificado_em_memoria.php b/examples/contribuinte/emitir_com_certificado_em_memoria.php new file mode 100644 index 0000000..2ef0995 --- /dev/null +++ b/examples/contribuinte/emitir_com_certificado_em_memoria.php @@ -0,0 +1,94 @@ +query("SELECT conteudo FROM certificados WHERE id = 1")->fetchColumn(); + +// 2. Lendo de variável de ambiente em base64 +// $pfxContent = base64_decode(getenv('CERTIFICADO_PFX_BASE64')); + +// 3. Lendo de um arquivo (equivalente ao comportamento anterior) +$pfxContent = file_get_contents(__DIR__.'/../certs/contribuinte.pfx'); + +$certificatePassword = '[PASSWORD]'; + +// Passa certificateContent em vez de certificatePath +$context = new NfseContext( + ambiente: TipoAmbiente::Homologacao, + certificatePath: null, + certificatePassword: $certificatePassword, + certificateContent: $pfxContent, +); + +$nfse = new Nfse($context); + +try { + $cnpjPrestador = '03279735000194'; + $codigoMunicipio = '2304400'; + $serie = '1'; + $numero = '100'; + + $idDps = IdGenerator::generateDpsId( + cpfCnpj: $cnpjPrestador, + codIbge: $codigoMunicipio, + serieDps: $serie, + numDps: $numero + ); + + $dps = new DpsData([ + '@attributes' => ['versao' => '1.00'], + 'infDPS' => [ + '@attributes' => ['Id' => $idDps], + 'tpAmb' => 2, + 'dhEmi' => date('c'), + 'verAplic' => 'SDK-PHP-1.0', + 'serie' => $serie, + 'nDPS' => $numero, + 'dCompet' => date('Y-m-d'), + 'tpEmit' => 1, + 'cLocEmi' => $codigoMunicipio, + 'prest' => [ + 'CNPJ' => $cnpjPrestador, + 'xNome' => 'Empresa de Teste', + 'end' => [ + 'endNac' => ['cMun' => $codigoMunicipio, 'CEP' => '60000000'], + 'xLgr' => 'Rua Teste', + 'nro' => '123', + 'xBairro' => 'Centro', + ], + 'regTrib' => ['opSimpNac' => 1, 'regEspTrib' => 0], + ], + 'toma' => [ + 'CNPJ' => '44827692000111', + 'xNome' => 'Cliente de Teste', + ], + 'serv' => [ + 'locPrest' => ['cLocPrestacao' => $codigoMunicipio], + 'cServ' => ['cTribNac' => '010101', 'xDescServ' => 'Desenvolvimento de Software'], + ], + 'valores' => [ + 'vServPrest' => ['vServ' => 100.00], + 'trib' => [ + 'tribMun' => ['tribISSQN' => 1, 'tpRetISSQN' => 1], + 'tribFed' => ['piscofins' => ['CST' => '08']], + 'totTrib' => ['indTotTrib' => 0], + ], + ], + ], + ]); + + $nfseData = $nfse->contribuinte()->emitir($dps); + + echo "NFS-e emitida com sucesso!\n"; + echo 'Chave de Acesso: '.$nfseData->infNfse->id."\n"; + +} catch (\Exception $e) { + echo 'Erro: '.$e->getMessage()."\n"; +} diff --git a/src/Http/Client/AdnClient.php b/src/Http/Client/AdnClient.php index a3be829..a8c07b0 100644 --- a/src/Http/Client/AdnClient.php +++ b/src/Http/Client/AdnClient.php @@ -26,11 +26,32 @@ class AdnClient implements AdnDanfseInterface private Client $httpClient; + private ?string $tempCertFile = null; + public function __construct(private NfseContext $context) { $this->httpClient = $this->createHttpClient(); } + public function __destruct() + { + if ($this->tempCertFile !== null && file_exists($this->tempCertFile)) { + unlink($this->tempCertFile); + } + } + + private function resolveCertificatePath(): string + { + if ($this->context->certificatePath !== null) { + return $this->context->certificatePath; + } + + $this->tempCertFile = tempnam(sys_get_temp_dir(), 'nfse_cert_'); + file_put_contents($this->tempCertFile, $this->context->certificateContent); + + return $this->tempCertFile; + } + private function createHttpClient(): Client { $baseUrl = $this->context->ambiente === TipoAmbiente::Producao @@ -41,7 +62,7 @@ private function createHttpClient(): Client 'base_uri' => $baseUrl, 'curl' => [ CURLOPT_SSLCERTTYPE => 'P12', - CURLOPT_SSLCERT => $this->context->certificatePath, + CURLOPT_SSLCERT => $this->resolveCertificatePath(), CURLOPT_SSLCERTPASSWD => $this->context->certificatePassword, CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, CURLOPT_CONNECTTIMEOUT => 30, diff --git a/src/Http/Client/CncClient.php b/src/Http/Client/CncClient.php index d5eccc5..e3aaf80 100644 --- a/src/Http/Client/CncClient.php +++ b/src/Http/Client/CncClient.php @@ -18,11 +18,32 @@ class CncClient private Client $httpClient; + private ?string $tempCertFile = null; + public function __construct(private NfseContext $context) { $this->httpClient = $this->createHttpClient(); } + public function __destruct() + { + if ($this->tempCertFile !== null && file_exists($this->tempCertFile)) { + unlink($this->tempCertFile); + } + } + + private function resolveCertificatePath(): string + { + if ($this->context->certificatePath !== null) { + return $this->context->certificatePath; + } + + $this->tempCertFile = tempnam(sys_get_temp_dir(), 'nfse_cert_'); + file_put_contents($this->tempCertFile, $this->context->certificateContent); + + return $this->tempCertFile; + } + private function createHttpClient(): Client { $baseUrl = $this->context->ambiente === TipoAmbiente::Producao @@ -33,7 +54,7 @@ private function createHttpClient(): Client 'base_uri' => $baseUrl, 'curl' => [ CURLOPT_SSLCERTTYPE => 'P12', - CURLOPT_SSLCERT => $this->context->certificatePath, + CURLOPT_SSLCERT => $this->resolveCertificatePath(), CURLOPT_SSLCERTPASSWD => $this->context->certificatePassword, CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, CURLOPT_CONNECTTIMEOUT => 30, diff --git a/src/Http/Client/SefinClient.php b/src/Http/Client/SefinClient.php index 1b70124..83c7753 100644 --- a/src/Http/Client/SefinClient.php +++ b/src/Http/Client/SefinClient.php @@ -21,6 +21,8 @@ class SefinClient implements SefinNacionalInterface private string $baseUrl; + private ?string $tempCertFile = null; + public function __construct(private NfseContext $context) { $resolver = new SefinEndpointResolver(); @@ -28,13 +30,32 @@ public function __construct(private NfseContext $context) $this->httpClient = $this->createHttpClient(); } + public function __destruct() + { + if ($this->tempCertFile !== null && file_exists($this->tempCertFile)) { + unlink($this->tempCertFile); + } + } + + private function resolveCertificatePath(): string + { + if ($this->context->certificatePath !== null) { + return $this->context->certificatePath; + } + + $this->tempCertFile = tempnam(sys_get_temp_dir(), 'nfse_cert_'); + file_put_contents($this->tempCertFile, $this->context->certificateContent); + + return $this->tempCertFile; + } + private function createHttpClient(): Client { return new Client([ 'base_uri' => rtrim($this->baseUrl, '/') . '/', 'curl' => [ CURLOPT_SSLCERTTYPE => 'P12', - CURLOPT_SSLCERT => $this->context->certificatePath, + CURLOPT_SSLCERT => $this->resolveCertificatePath(), CURLOPT_SSLCERTPASSWD => $this->context->certificatePassword, CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, CURLOPT_CONNECTTIMEOUT => 30, diff --git a/src/Http/NfseContext.php b/src/Http/NfseContext.php index 02a95d7..4d178b0 100644 --- a/src/Http/NfseContext.php +++ b/src/Http/NfseContext.php @@ -8,9 +8,14 @@ final class NfseContext { public function __construct( public TipoAmbiente $ambiente, - public string $certificatePath, + public ?string $certificatePath, public string $certificatePassword, public ?string $codigoMunicipio = null, public ?\Nfse\Dto\Http\Endpoint $endpoint = null, - ) {} + public ?string $certificateContent = null, + ) { + if ($certificatePath === null && $certificateContent === null) { + throw new \InvalidArgumentException('Informe certificatePath ou certificateContent.'); + } + } } \ No newline at end of file diff --git a/src/Service/ContribuinteService.php b/src/Service/ContribuinteService.php index 6a9c748..0990b53 100644 --- a/src/Service/ContribuinteService.php +++ b/src/Service/ContribuinteService.php @@ -37,7 +37,7 @@ public function emitir(DpsData $dps): NfseData $builder = new DpsXmlBuilder; $xml = $builder->build($dps); - $cert = new Certificate($this->context->certificatePath, $this->context->certificatePassword); + $cert = $this->makeCertificate(); $signer = $this->createSigner($cert); // Assina a tag 'infDPS' @@ -112,7 +112,7 @@ public function registrarEventoData(PedRegEventoData $evento): \Nfse\Dto\Http\Re $builder = new EventosXmlBuilder; $xml = $builder->buildPedRegEvento($evento); - $cert = new Certificate($this->context->certificatePath, $this->context->certificatePassword); + $cert = $this->makeCertificate(); $signer = $this->createSigner($cert); // Assina a tag 'infPedReg' @@ -199,4 +199,13 @@ protected function createSigner(Certificate $certificate): SignerInterface { return new XmlSigner($certificate); } + + private function makeCertificate(): Certificate + { + if ($this->context->certificateContent !== null) { + return Certificate::fromContent($this->context->certificateContent, $this->context->certificatePassword); + } + + return new Certificate($this->context->certificatePath, $this->context->certificatePassword); + } } diff --git a/src/Signer/Certificate.php b/src/Signer/Certificate.php index 9b47c91..4c0184c 100644 --- a/src/Signer/Certificate.php +++ b/src/Signer/Certificate.php @@ -23,6 +23,16 @@ public function __construct(string $pfxPath, string $password) $this->load(); } + public static function fromContent(string $pfxContent, string $password): self + { + $instance = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor(); + $instance->pfxContent = $pfxContent; + $instance->password = $password; + $instance->load(); + + return $instance; + } + private function load(): void { if (! openssl_pkcs12_read($this->pfxContent, $certs, $this->password)) { diff --git a/tests/Unit/Signer/CertificateTest.php b/tests/Unit/Signer/CertificateTest.php index d3db096..e999e90 100644 --- a/tests/Unit/Signer/CertificateTest.php +++ b/tests/Unit/Signer/CertificateTest.php @@ -35,3 +35,37 @@ ->and($cert->getCleanCertificate())->not->toContain('BEGIN CERTIFICATE') ->and($cert->sign('content'))->toBeString(); }); + +it('fromContent loads certificate from raw bytes', function () { + $pfxContent = file_get_contents(__DIR__.'/../../fixtures/certs/test.pfx'); + + $cert = Certificate::fromContent($pfxContent, '1234'); + + expect($cert->getPrivateKey())->toBeString() + ->and($cert->getCertificate())->toBeString() + ->and($cert->getCleanCertificate())->not->toContain('BEGIN CERTIFICATE') + ->and($cert->sign('content'))->toBeString(); +}); + +it('fromContent produces the same output as fromPath', function () { + $pfxPath = __DIR__.'/../../fixtures/certs/test.pfx'; + $pfxContent = file_get_contents($pfxPath); + + $certFromPath = new Certificate($pfxPath, '1234'); + $certFromContent = Certificate::fromContent($pfxContent, '1234'); + + expect($certFromContent->getCertificate())->toBe($certFromPath->getCertificate()) + ->and($certFromContent->getCleanCertificate())->toBe($certFromPath->getCleanCertificate()); +}); + +it('fromContent throws on wrong password', function () { + $pfxContent = file_get_contents(__DIR__.'/../../fixtures/certs/test.pfx'); + + expect(fn () => Certificate::fromContent($pfxContent, 'wrong_password')) + ->toThrow(Exception::class, 'Senha do certificado incorreta ou arquivo inválido/corrompido'); +}); + +it('fromContent throws on invalid pfx bytes', function () { + expect(fn () => Certificate::fromContent('not-a-pfx-file', 'any')) + ->toThrow(Exception::class, 'Senha do certificado incorreta ou arquivo inválido/corrompido'); +});