Skip to content

Commit c3b475a

Browse files
authored
Certificado digital em memória (#40)
1 parent eb6201e commit c3b475a

8 files changed

Lines changed: 222 additions & 7 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
use Nfse\Dto\Nfse\DpsData;
4+
use Nfse\Enums\TipoAmbiente;
5+
use Nfse\Http\NfseContext;
6+
use Nfse\Nfse;
7+
use Nfse\Support\IdGenerator;
8+
9+
require_once __DIR__.'/../../vendor/autoload.php';
10+
11+
// 1. Lendo de banco de dados
12+
// $pfxContent = $pdo->query("SELECT conteudo FROM certificados WHERE id = 1")->fetchColumn();
13+
14+
// 2. Lendo de variável de ambiente em base64
15+
// $pfxContent = base64_decode(getenv('CERTIFICADO_PFX_BASE64'));
16+
17+
// 3. Lendo de um arquivo (equivalente ao comportamento anterior)
18+
$pfxContent = file_get_contents(__DIR__.'/../certs/contribuinte.pfx');
19+
20+
$certificatePassword = '[PASSWORD]';
21+
22+
// Passa certificateContent em vez de certificatePath
23+
$context = new NfseContext(
24+
ambiente: TipoAmbiente::Homologacao,
25+
certificatePath: null,
26+
certificatePassword: $certificatePassword,
27+
certificateContent: $pfxContent,
28+
);
29+
30+
$nfse = new Nfse($context);
31+
32+
try {
33+
$cnpjPrestador = '03279735000194';
34+
$codigoMunicipio = '2304400';
35+
$serie = '1';
36+
$numero = '100';
37+
38+
$idDps = IdGenerator::generateDpsId(
39+
cpfCnpj: $cnpjPrestador,
40+
codIbge: $codigoMunicipio,
41+
serieDps: $serie,
42+
numDps: $numero
43+
);
44+
45+
$dps = new DpsData([
46+
'@attributes' => ['versao' => '1.00'],
47+
'infDPS' => [
48+
'@attributes' => ['Id' => $idDps],
49+
'tpAmb' => 2,
50+
'dhEmi' => date('c'),
51+
'verAplic' => 'SDK-PHP-1.0',
52+
'serie' => $serie,
53+
'nDPS' => $numero,
54+
'dCompet' => date('Y-m-d'),
55+
'tpEmit' => 1,
56+
'cLocEmi' => $codigoMunicipio,
57+
'prest' => [
58+
'CNPJ' => $cnpjPrestador,
59+
'xNome' => 'Empresa de Teste',
60+
'end' => [
61+
'endNac' => ['cMun' => $codigoMunicipio, 'CEP' => '60000000'],
62+
'xLgr' => 'Rua Teste',
63+
'nro' => '123',
64+
'xBairro' => 'Centro',
65+
],
66+
'regTrib' => ['opSimpNac' => 1, 'regEspTrib' => 0],
67+
],
68+
'toma' => [
69+
'CNPJ' => '44827692000111',
70+
'xNome' => 'Cliente de Teste',
71+
],
72+
'serv' => [
73+
'locPrest' => ['cLocPrestacao' => $codigoMunicipio],
74+
'cServ' => ['cTribNac' => '010101', 'xDescServ' => 'Desenvolvimento de Software'],
75+
],
76+
'valores' => [
77+
'vServPrest' => ['vServ' => 100.00],
78+
'trib' => [
79+
'tribMun' => ['tribISSQN' => 1, 'tpRetISSQN' => 1],
80+
'tribFed' => ['piscofins' => ['CST' => '08']],
81+
'totTrib' => ['indTotTrib' => 0],
82+
],
83+
],
84+
],
85+
]);
86+
87+
$nfseData = $nfse->contribuinte()->emitir($dps);
88+
89+
echo "NFS-e emitida com sucesso!\n";
90+
echo 'Chave de Acesso: '.$nfseData->infNfse->id."\n";
91+
92+
} catch (\Exception $e) {
93+
echo 'Erro: '.$e->getMessage()."\n";
94+
}

src/Http/Client/AdnClient.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,32 @@ class AdnClient implements AdnDanfseInterface
2626

2727
private Client $httpClient;
2828

29+
private ?string $tempCertFile = null;
30+
2931
public function __construct(private NfseContext $context)
3032
{
3133
$this->httpClient = $this->createHttpClient();
3234
}
3335

36+
public function __destruct()
37+
{
38+
if ($this->tempCertFile !== null && file_exists($this->tempCertFile)) {
39+
unlink($this->tempCertFile);
40+
}
41+
}
42+
43+
private function resolveCertificatePath(): string
44+
{
45+
if ($this->context->certificatePath !== null) {
46+
return $this->context->certificatePath;
47+
}
48+
49+
$this->tempCertFile = tempnam(sys_get_temp_dir(), 'nfse_cert_');
50+
file_put_contents($this->tempCertFile, $this->context->certificateContent);
51+
52+
return $this->tempCertFile;
53+
}
54+
3455
private function createHttpClient(): Client
3556
{
3657
$baseUrl = $this->context->ambiente === TipoAmbiente::Producao
@@ -41,7 +62,7 @@ private function createHttpClient(): Client
4162
'base_uri' => $baseUrl,
4263
'curl' => [
4364
CURLOPT_SSLCERTTYPE => 'P12',
44-
CURLOPT_SSLCERT => $this->context->certificatePath,
65+
CURLOPT_SSLCERT => $this->resolveCertificatePath(),
4566
CURLOPT_SSLCERTPASSWD => $this->context->certificatePassword,
4667
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
4768
CURLOPT_CONNECTTIMEOUT => 30,

src/Http/Client/CncClient.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,32 @@ class CncClient
1818

1919
private Client $httpClient;
2020

21+
private ?string $tempCertFile = null;
22+
2123
public function __construct(private NfseContext $context)
2224
{
2325
$this->httpClient = $this->createHttpClient();
2426
}
2527

28+
public function __destruct()
29+
{
30+
if ($this->tempCertFile !== null && file_exists($this->tempCertFile)) {
31+
unlink($this->tempCertFile);
32+
}
33+
}
34+
35+
private function resolveCertificatePath(): string
36+
{
37+
if ($this->context->certificatePath !== null) {
38+
return $this->context->certificatePath;
39+
}
40+
41+
$this->tempCertFile = tempnam(sys_get_temp_dir(), 'nfse_cert_');
42+
file_put_contents($this->tempCertFile, $this->context->certificateContent);
43+
44+
return $this->tempCertFile;
45+
}
46+
2647
private function createHttpClient(): Client
2748
{
2849
$baseUrl = $this->context->ambiente === TipoAmbiente::Producao
@@ -33,7 +54,7 @@ private function createHttpClient(): Client
3354
'base_uri' => $baseUrl,
3455
'curl' => [
3556
CURLOPT_SSLCERTTYPE => 'P12',
36-
CURLOPT_SSLCERT => $this->context->certificatePath,
57+
CURLOPT_SSLCERT => $this->resolveCertificatePath(),
3758
CURLOPT_SSLCERTPASSWD => $this->context->certificatePassword,
3859
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
3960
CURLOPT_CONNECTTIMEOUT => 30,

src/Http/Client/SefinClient.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,41 @@ class SefinClient implements SefinNacionalInterface
2121

2222
private string $baseUrl;
2323

24+
private ?string $tempCertFile = null;
25+
2426
public function __construct(private NfseContext $context)
2527
{
2628
$resolver = new SefinEndpointResolver();
2729
$this->baseUrl = $resolver->resolve($this->context);
2830
$this->httpClient = $this->createHttpClient();
2931
}
3032

33+
public function __destruct()
34+
{
35+
if ($this->tempCertFile !== null && file_exists($this->tempCertFile)) {
36+
unlink($this->tempCertFile);
37+
}
38+
}
39+
40+
private function resolveCertificatePath(): string
41+
{
42+
if ($this->context->certificatePath !== null) {
43+
return $this->context->certificatePath;
44+
}
45+
46+
$this->tempCertFile = tempnam(sys_get_temp_dir(), 'nfse_cert_');
47+
file_put_contents($this->tempCertFile, $this->context->certificateContent);
48+
49+
return $this->tempCertFile;
50+
}
51+
3152
private function createHttpClient(): Client
3253
{
3354
return new Client([
3455
'base_uri' => rtrim($this->baseUrl, '/') . '/',
3556
'curl' => [
3657
CURLOPT_SSLCERTTYPE => 'P12',
37-
CURLOPT_SSLCERT => $this->context->certificatePath,
58+
CURLOPT_SSLCERT => $this->resolveCertificatePath(),
3859
CURLOPT_SSLCERTPASSWD => $this->context->certificatePassword,
3960
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
4061
CURLOPT_CONNECTTIMEOUT => 30,

src/Http/NfseContext.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ final class NfseContext
88
{
99
public function __construct(
1010
public TipoAmbiente $ambiente,
11-
public string $certificatePath,
11+
public ?string $certificatePath,
1212
public string $certificatePassword,
1313
public ?string $codigoMunicipio = null,
1414
public ?\Nfse\Dto\Http\Endpoint $endpoint = null,
15-
) {}
15+
public ?string $certificateContent = null,
16+
) {
17+
if ($certificatePath === null && $certificateContent === null) {
18+
throw new \InvalidArgumentException('Informe certificatePath ou certificateContent.');
19+
}
20+
}
1621
}

src/Service/ContribuinteService.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function emitir(DpsData $dps): NfseData
3737
$builder = new DpsXmlBuilder;
3838
$xml = $builder->build($dps);
3939

40-
$cert = new Certificate($this->context->certificatePath, $this->context->certificatePassword);
40+
$cert = $this->makeCertificate();
4141
$signer = $this->createSigner($cert);
4242

4343
// Assina a tag 'infDPS'
@@ -112,7 +112,7 @@ public function registrarEventoData(PedRegEventoData $evento): \Nfse\Dto\Http\Re
112112
$builder = new EventosXmlBuilder;
113113
$xml = $builder->buildPedRegEvento($evento);
114114

115-
$cert = new Certificate($this->context->certificatePath, $this->context->certificatePassword);
115+
$cert = $this->makeCertificate();
116116
$signer = $this->createSigner($cert);
117117

118118
// Assina a tag 'infPedReg'
@@ -199,4 +199,13 @@ protected function createSigner(Certificate $certificate): SignerInterface
199199
{
200200
return new XmlSigner($certificate);
201201
}
202+
203+
private function makeCertificate(): Certificate
204+
{
205+
if ($this->context->certificateContent !== null) {
206+
return Certificate::fromContent($this->context->certificateContent, $this->context->certificatePassword);
207+
}
208+
209+
return new Certificate($this->context->certificatePath, $this->context->certificatePassword);
210+
}
202211
}

src/Signer/Certificate.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ public function __construct(string $pfxPath, string $password)
2323
$this->load();
2424
}
2525

26+
public static function fromContent(string $pfxContent, string $password): self
27+
{
28+
$instance = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor();
29+
$instance->pfxContent = $pfxContent;
30+
$instance->password = $password;
31+
$instance->load();
32+
33+
return $instance;
34+
}
35+
2636
private function load(): void
2737
{
2838
if (! openssl_pkcs12_read($this->pfxContent, $certs, $this->password)) {

tests/Unit/Signer/CertificateTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,37 @@
3535
->and($cert->getCleanCertificate())->not->toContain('BEGIN CERTIFICATE')
3636
->and($cert->sign('content'))->toBeString();
3737
});
38+
39+
it('fromContent loads certificate from raw bytes', function () {
40+
$pfxContent = file_get_contents(__DIR__.'/../../fixtures/certs/test.pfx');
41+
42+
$cert = Certificate::fromContent($pfxContent, '1234');
43+
44+
expect($cert->getPrivateKey())->toBeString()
45+
->and($cert->getCertificate())->toBeString()
46+
->and($cert->getCleanCertificate())->not->toContain('BEGIN CERTIFICATE')
47+
->and($cert->sign('content'))->toBeString();
48+
});
49+
50+
it('fromContent produces the same output as fromPath', function () {
51+
$pfxPath = __DIR__.'/../../fixtures/certs/test.pfx';
52+
$pfxContent = file_get_contents($pfxPath);
53+
54+
$certFromPath = new Certificate($pfxPath, '1234');
55+
$certFromContent = Certificate::fromContent($pfxContent, '1234');
56+
57+
expect($certFromContent->getCertificate())->toBe($certFromPath->getCertificate())
58+
->and($certFromContent->getCleanCertificate())->toBe($certFromPath->getCleanCertificate());
59+
});
60+
61+
it('fromContent throws on wrong password', function () {
62+
$pfxContent = file_get_contents(__DIR__.'/../../fixtures/certs/test.pfx');
63+
64+
expect(fn () => Certificate::fromContent($pfxContent, 'wrong_password'))
65+
->toThrow(Exception::class, 'Senha do certificado incorreta ou arquivo inválido/corrompido');
66+
});
67+
68+
it('fromContent throws on invalid pfx bytes', function () {
69+
expect(fn () => Certificate::fromContent('not-a-pfx-file', 'any'))
70+
->toThrow(Exception::class, 'Senha do certificado incorreta ou arquivo inválido/corrompido');
71+
});

0 commit comments

Comments
 (0)