1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace NFePHP\Common\Certificate; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* Classe para tratamento e uso dos certificados digitais modelo A1 (PKCS12) |
7
|
|
|
* |
8
|
|
|
* @category NFePHP |
9
|
|
|
* @package NFePHP\Common\Certificate |
10
|
|
|
* @copyright Copyright (c) 2008-2014 |
11
|
|
|
* @license http://www.gnu.org/licenses/lesser.html LGPL v3 |
12
|
|
|
* @author Roberto L. Machado <linux.rlm at gmail dot com> |
13
|
|
|
* @link http://github.com/nfephp-org/nfephp for the canonical source repository |
14
|
|
|
*/ |
15
|
|
|
|
16
|
|
|
use NFePHP\Common\Certificate\Asn; |
17
|
|
|
use NFePHP\Common\Exception; |
18
|
|
|
use NFePHP\Common\Dom\Dom; |
19
|
|
|
|
20
|
|
|
class Pkcs12 |
21
|
|
|
{ |
22
|
|
|
/** |
23
|
|
|
* Path para o diretorio onde o arquivo pfx está localizado |
24
|
|
|
* @var string |
25
|
|
|
*/ |
26
|
|
|
public $pathCerts = ''; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Path para o arquivo pfx (certificado digital em formato de transporte) |
30
|
|
|
* @var string |
31
|
|
|
*/ |
32
|
|
|
public $pfxFileName = ''; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Conteudo do arquivo pfx |
36
|
|
|
* @var string |
37
|
|
|
*/ |
38
|
|
|
public $pfxCert = ''; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Numero do CNPJ do emitente |
42
|
|
|
* @var string |
43
|
|
|
*/ |
44
|
|
|
public $cnpj = ''; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* String que contêm a chave publica em formato PEM |
48
|
|
|
* @var string |
49
|
|
|
*/ |
50
|
|
|
public $pubKey = ''; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* String quem contêm a chave privada em formato PEM |
54
|
|
|
* @var string |
55
|
|
|
*/ |
56
|
|
|
public $priKey = ''; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* String que conten a combinação da chave publica e privada em formato PEM |
60
|
|
|
* e a cadeida completa de certificação caso exista |
61
|
|
|
* @var string |
62
|
|
|
*/ |
63
|
|
|
public $certKey = ''; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Flag para ignorar testes de validade do certificado |
67
|
|
|
* isso é usado apenas para fins de testes |
68
|
|
|
* @var boolean |
69
|
|
|
*/ |
70
|
|
|
public $ignoreValidCert = false; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Path para a chave publica em arquivo |
74
|
|
|
* @var string |
75
|
|
|
*/ |
76
|
|
|
public $pubKeyFile = ''; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Path para a chave privada em arquivo |
80
|
|
|
* @var string |
81
|
|
|
*/ |
82
|
|
|
public $priKeyFile = ''; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Path para o certificado em arquivo |
86
|
|
|
* @var string |
87
|
|
|
*/ |
88
|
|
|
public $certKeyFile = ''; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Timestamp da data de validade do certificado |
92
|
|
|
* @var float |
93
|
|
|
*/ |
94
|
|
|
public $expireTimestamp = 0; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Mensagem de erro da classe |
98
|
|
|
* @var string |
99
|
|
|
*/ |
100
|
|
|
public $error = ''; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Id do docimento sendo assinado |
104
|
|
|
* @var string |
105
|
|
|
*/ |
106
|
|
|
public $docId = ''; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Método de construção da classe |
110
|
|
|
* @param string $pathCerts Path para a pasta que contêm os certificados digitais |
111
|
|
|
* @param string $cnpj CNPJ do emitente, sem ./-, apenas os numeros |
112
|
|
|
* @param string $pubKey Chave publica em formato PEM, não o path mas a chave em si |
113
|
|
|
* @param string $priKey Chave privada em formato PEM, não o path mas a chave em si |
114
|
|
|
* @param string $certKey Certificado em formato PEM, não o path mas a chave em si |
115
|
|
|
* @param bool $ignoreValidCert |
116
|
|
|
* @param boolean $ignoreValidCert Ignora a validade do certificado, mais usado para fins de teste |
117
|
|
|
*/ |
118
|
24 |
|
public function __construct( |
119
|
|
|
$pathCerts = '', |
120
|
|
|
$cnpj = '', |
121
|
|
|
$pubKey = '', |
122
|
|
|
$priKey = '', |
123
|
|
|
$certKey = '', |
124
|
|
|
$ignoreValidCert = false |
125
|
|
|
) { |
126
|
24 |
|
$ncnpj = preg_replace('/[^0-9]/', '', $cnpj); |
127
|
24 |
|
if (empty($pathCerts)) { |
128
|
|
|
//estabelecer diretorio default |
129
|
|
|
$pathCerts = dirname(dirname(dirname(dirname(__FILE__)))).DIRECTORY_SEPARATOR.'certs'.DIRECTORY_SEPARATOR; |
130
|
|
|
} |
131
|
24 |
|
if (! empty($pathCerts)) { |
132
|
24 |
|
if (!is_dir(trim($pathCerts))) { |
133
|
3 |
|
throw new Exception\InvalidArgumentException( |
134
|
|
|
"Um path válido para os certificados deve ser passado." |
135
|
3 |
|
. " Diretório [$pathCerts] não foi localizado." |
136
|
3 |
|
); |
137
|
|
|
} |
138
|
21 |
|
$this->pathCerts = trim($pathCerts); |
139
|
21 |
|
} |
140
|
21 |
|
$this->ignoreValidCert = $ignoreValidCert; |
141
|
21 |
|
$flagCert = false; |
142
|
21 |
|
if ($pubKey != '' && $priKey != '' && strlen($pubKey) > 500 && strlen($priKey) > 500) { |
143
|
9 |
|
$this->pubKey = $pubKey; |
144
|
9 |
|
$this->priKey = $priKey; |
145
|
9 |
|
$this->certKey = $priKey."\r\n".$pubKey; |
146
|
9 |
|
$flagCert = true; |
147
|
9 |
|
} |
148
|
21 |
|
if ($certKey != '') { |
149
|
|
|
$this->certKey = $certKey; |
150
|
|
|
} |
151
|
21 |
|
$this->cnpj = $ncnpj; |
152
|
21 |
|
if (! $this->zInit($flagCert)) { |
153
|
3 |
|
throw new Exception\RuntimeException($this->error); |
154
|
|
|
} |
155
|
18 |
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* zInit |
159
|
|
|
* Método de inicialização da classe irá verificar |
160
|
|
|
* os parâmetros, arquivos e validade dos mesmos |
161
|
|
|
* Em caso de erro o motivo da falha será indicada na parâmetro |
162
|
|
|
* error da classe, os outros parâmetros serão limpos e os |
163
|
|
|
* arquivos inválidos serão removidos da pasta |
164
|
|
|
* @param boolean $flagCert indica que as chaves já foram passas como strings |
165
|
|
|
* @return boolean |
166
|
|
|
*/ |
167
|
21 |
|
private function zInit($flagCert = false) |
168
|
|
|
{ |
169
|
|
|
//se as chaves foram passadas na forma de strings então verificar a validade |
170
|
21 |
|
if ($flagCert) { |
171
|
|
|
//já que o certificado existe, verificar seu prazo de validade |
172
|
|
|
//o certificado será removido se estiver vencido |
173
|
9 |
|
if (!$this->ignoreValidCert) { |
174
|
3 |
|
return $this->zValidCerts($this->pubKey); |
175
|
|
|
} |
176
|
6 |
|
} else { |
177
|
12 |
|
if (substr($this->pathCerts, -1) !== DIRECTORY_SEPARATOR) { |
178
|
|
|
$this->pathCerts .= DIRECTORY_SEPARATOR; |
179
|
|
|
} |
180
|
|
|
//monta o path completo com o nome da chave privada |
181
|
12 |
|
$this->priKeyFile = $this->pathCerts.$this->cnpj.'_priKEY.pem'; |
182
|
|
|
//monta o path completo com o nome da chave publica |
183
|
12 |
|
$this->pubKeyFile = $this->pathCerts.$this->cnpj.'_pubKEY.pem'; |
184
|
|
|
//monta o path completo com o nome do certificado (chave publica e privada) em formato pem |
185
|
12 |
|
$this->certKeyFile = $this->pathCerts.$this->cnpj.'_certKEY.pem'; |
186
|
|
|
//se as chaves não foram passadas em strings, verifica se os certificados existem |
187
|
12 |
|
if (is_file($this->priKeyFile) && is_file($this->pubKeyFile) && is_file($this->certKeyFile)) { |
188
|
|
|
//se as chaves existem deve ser verificado sua validade |
189
|
|
|
$this->pubKey = file_get_contents($this->pubKeyFile); |
190
|
|
|
$this->priKey = file_get_contents($this->priKeyFile); |
191
|
|
|
$this->certKey = file_get_contents($this->certKeyFile); |
192
|
|
|
//já que o certificado existe, verificar seu prazo de validade |
193
|
|
|
if (! $this->ignoreValidCert) { |
194
|
|
|
return $this->zValidCerts($this->pubKey); |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
} |
198
|
18 |
|
return true; |
199
|
|
|
}//fim init |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* loadPfxFile |
203
|
|
|
* @param string $pathPfx caminho completo para o arquivo pfx |
204
|
|
|
* @param string $password senha para abrir o certificado pfx |
205
|
|
|
* @param bool $createFiles |
206
|
|
|
* @param bool $ignoreValidity |
207
|
|
|
* @param bool $ignoreOwner |
208
|
|
|
* @return bool |
209
|
|
|
*/ |
210
|
3 |
|
public function loadPfxFile( |
211
|
|
|
$pathPfx = '', |
212
|
|
|
$password = '', |
213
|
|
|
$createFiles = true, |
214
|
|
|
$ignoreValidity = false, |
215
|
|
|
$ignoreOwner = false |
216
|
|
|
) { |
217
|
3 |
|
if (! is_file($pathPfx)) { |
218
|
|
|
throw new Exception\InvalidArgumentException( |
219
|
|
|
"O nome do arquivo PFX deve ser passado. Não foi localizado o arquivo [$pathPfx]." |
220
|
|
|
); |
221
|
|
|
} |
222
|
3 |
|
$this->pfxCert = file_get_contents($pathPfx); |
223
|
3 |
|
return $this->loadPfx($this->pfxCert, $password, $createFiles, $ignoreValidity, $ignoreOwner); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* loadPfx |
228
|
|
|
* Carrega um novo certificado no formato PFX |
229
|
|
|
* Isso deverá ocorrer a cada atualização do certificado digital, ou seja, |
230
|
|
|
* pelo menos uma vez por ano, uma vez que a validade do certificado |
231
|
|
|
* é anual. |
232
|
|
|
* Será verificado também se o certificado pertence realmente ao CNPJ |
233
|
|
|
* Essa verificação checa apenas se o certificado pertence a matriz ou filial |
234
|
|
|
* comparando apenas os primeiros 8 digitos do CNPJ, dessa forma ambas a |
235
|
|
|
* matriz e as filiais poderão usar o mesmo certificado indicado na instanciação |
236
|
|
|
* da classe, se não for um erro irá ocorrer e |
237
|
|
|
* o certificado não será convertido para o formato PEM. |
238
|
|
|
* Em caso de erros, será retornado false e o motivo será indicado no |
239
|
|
|
* parâmetro error da classe. |
240
|
|
|
* Os certificados serão armazenados como <CNPJ>-<tipo>.pem |
241
|
|
|
* @param string $pfxContent arquivo PFX |
242
|
|
|
* @param string $password Senha de acesso ao certificado PFX |
243
|
|
|
* @param boolean $createFiles se true irá criar os arquivos pem das chaves digitais, caso contrario não |
244
|
|
|
* @param bool $ignoreValidity |
245
|
|
|
* @param bool $ignoreOwner |
246
|
|
|
* @return bool |
247
|
|
|
*/ |
248
|
9 |
|
public function loadPfx( |
249
|
|
|
$pfxContent = '', |
250
|
|
|
$password = '', |
251
|
|
|
$createFiles = true, |
252
|
|
|
$ignoreValidity = false, |
253
|
|
|
$ignoreOwner = false |
254
|
|
|
) { |
255
|
9 |
|
if ($password == '') { |
256
|
|
|
throw new Exception\InvalidArgumentException( |
257
|
|
|
"A senha de acesso para o certificado pfx não pode ser vazia." |
258
|
|
|
); |
259
|
|
|
} |
260
|
|
|
//carrega os certificados e chaves para um array denominado $x509certdata |
261
|
9 |
|
$x509certdata = array(); |
262
|
9 |
|
if (!openssl_pkcs12_read($pfxContent, $x509certdata, $password)) { |
263
|
|
|
throw new Exception\RuntimeException( |
264
|
|
|
"O certificado não pode ser lido!! Senha errada ou arquivo corrompido ou formato inválido!!" |
265
|
|
|
); |
266
|
|
|
} |
267
|
9 |
|
$this->pfxCert = $pfxContent; |
268
|
9 |
|
if (!$ignoreValidity) { |
269
|
|
|
//verifica sua data de validade |
270
|
|
|
if (! $this->zValidCerts($x509certdata['cert'])) { |
271
|
|
|
throw new Exception\RuntimeException($this->error); |
272
|
|
|
} |
273
|
|
|
} |
274
|
9 |
|
if (!$ignoreOwner) { |
275
|
3 |
|
$cnpjCert = Asn::getCNPJCert($x509certdata['cert']); |
276
|
3 |
|
if (substr($this->cnpj, 0, 8) != substr($cnpjCert, 0, 8)) { |
277
|
3 |
|
throw new Exception\InvalidArgumentException( |
278
|
|
|
"O Certificado fornecido pertence a outro CNPJ!!" |
279
|
3 |
|
); |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
//monta o path completo com o nome da chave privada |
283
|
6 |
|
$this->priKeyFile = $this->pathCerts.$this->cnpj.'_priKEY.pem'; |
284
|
|
|
//monta o path completo com o nome da chave publica |
285
|
6 |
|
$this->pubKeyFile = $this->pathCerts.$this->cnpj.'_pubKEY.pem'; |
286
|
|
|
//monta o path completo com o nome do certificado (chave publica e privada) em formato pem |
287
|
6 |
|
$this->certKeyFile = $this->pathCerts.$this->cnpj.'_certKEY.pem'; |
288
|
6 |
|
$this->zRemovePemFiles(); |
289
|
6 |
|
if ($createFiles) { |
290
|
|
|
$this->zSavePemFiles($x509certdata); |
291
|
|
|
} |
292
|
6 |
|
$this->pubKey=$x509certdata['cert']; |
293
|
6 |
|
$this->priKey=$x509certdata['pkey']; |
294
|
6 |
|
$this->certKey=$x509certdata['pkey']."\r\n".$x509certdata['cert']; |
295
|
6 |
|
return true; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* zSavePemFiles |
300
|
|
|
* @param array $x509certdata |
301
|
|
|
* @throws Exception\InvalidArgumentException |
302
|
|
|
* @throws Exception\RuntimeException |
303
|
|
|
*/ |
304
|
|
|
private function zSavePemFiles($x509certdata) |
305
|
|
|
{ |
306
|
|
|
if (empty($this->pathCerts)) { |
307
|
|
|
throw new Exception\InvalidArgumentException( |
308
|
|
|
"Não está definido o diretório para armazenar os certificados." |
309
|
|
|
); |
310
|
|
|
} |
311
|
|
|
if (! is_dir($this->pathCerts)) { |
312
|
|
|
throw new Exception\InvalidArgumentException( |
313
|
|
|
"Não existe o diretório para armazenar os certificados." |
314
|
|
|
); |
315
|
|
|
} |
316
|
|
|
//recriar os arquivos pem com o arquivo pfx |
317
|
|
|
if (!file_put_contents($this->priKeyFile, $x509certdata['pkey'])) { |
318
|
|
|
throw new Exception\RuntimeException( |
319
|
|
|
"Falha de permissão de escrita na pasta dos certificados!!" |
320
|
|
|
); |
321
|
|
|
} |
322
|
|
|
file_put_contents($this->pubKeyFile, $x509certdata['cert']); |
323
|
|
|
file_put_contents($this->certKeyFile, $x509certdata['pkey']."\r\n".$x509certdata['cert']); |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* aadChain |
328
|
|
|
* @param array $aCerts Array com os caminhos completos para cada certificado da cadeia |
329
|
|
|
* ou um array com o conteúdo desses certificados |
330
|
|
|
* @return void |
331
|
|
|
*/ |
332
|
3 |
|
public function aadChain($aCerts = array()) |
333
|
|
|
{ |
334
|
3 |
|
$certificate = $this->certKey; |
335
|
3 |
|
foreach ($aCerts as $cert) { |
336
|
3 |
|
if (is_file($cert)) { |
337
|
3 |
|
$dados = file_get_contents($cert); |
338
|
3 |
|
$certificate .= "\r\n" . $dados; |
339
|
3 |
|
} else { |
340
|
|
|
$certificate .= "\r\n" . $cert; |
341
|
|
|
} |
342
|
3 |
|
} |
343
|
3 |
|
$this->certKey = $certificate; |
344
|
3 |
|
if (is_file($this->certKeyFile)) { |
345
|
|
|
file_put_contents($this->certKeyFile, $certificate); |
346
|
|
|
} |
347
|
3 |
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* signXML |
351
|
|
|
* @param string $docxml |
352
|
|
|
* @param string $tagid |
353
|
|
|
* @param string $marcador |
354
|
|
|
* @param string $algorithm |
355
|
|
|
* @return string xml assinado |
356
|
|
|
* @throws Exception\InvalidArgumentException |
357
|
|
|
* @throws Exception\RuntimeException |
358
|
|
|
*/ |
359
|
3 |
|
public function signXML($docxml, $tagid = '', $marcador = 'Id', $algorithm = 'SHA1') |
360
|
|
|
{ |
361
|
|
|
//caso não tenha as chaves cai fora |
362
|
3 |
|
if ($this->pubKey == '' || $this->priKey == '') { |
363
|
|
|
$msg = "As chaves não estão disponíveis."; |
364
|
|
|
throw new Exception\InvalidArgumentException($msg); |
365
|
|
|
} |
366
|
|
|
//caso não seja informada a tag a ser assinada cai fora |
367
|
3 |
|
if ($tagid == '') { |
368
|
|
|
$msg = "A tag a ser assinada deve ser indicada."; |
369
|
|
|
throw new Exception\InvalidArgumentException($msg); |
370
|
|
|
} |
371
|
|
|
//carrega a chave privada no openssl |
372
|
3 |
|
$objSSLPriKey = openssl_get_privatekey($this->priKey); |
373
|
3 |
|
if ($objSSLPriKey === false) { |
374
|
|
|
$msg = "Houve erro no carregamento da chave privada."; |
375
|
|
|
$this->zGetOpenSSLError($msg); |
376
|
|
|
//while ($erro = openssl_error_string()) { |
|
|
|
|
377
|
|
|
// $msg .= $erro . "\n"; |
378
|
|
|
//} |
379
|
|
|
//throw new Exception\RuntimeException($msg); |
|
|
|
|
380
|
|
|
} |
381
|
3 |
|
$xml = $docxml; |
382
|
3 |
|
if (is_file($docxml)) { |
383
|
|
|
$xml = file_get_contents($docxml); |
384
|
|
|
} |
385
|
|
|
//remove sujeiras do xml |
386
|
3 |
|
$order = array("\r\n", "\n", "\r", "\t"); |
387
|
3 |
|
$xml = str_replace($order, '', $xml); |
388
|
3 |
|
$xmldoc = new Dom(); |
389
|
3 |
|
$xmldoc->loadXMLString($xml); |
390
|
|
|
//coloca o node raiz em uma variável |
391
|
3 |
|
$root = $xmldoc->documentElement; |
392
|
|
|
//extrair a tag com os dados a serem assinados |
393
|
3 |
|
$node = $xmldoc->getElementsByTagName($tagid)->item(0); |
394
|
3 |
|
if (!isset($node)) { |
395
|
|
|
throw new Exception\RuntimeException( |
396
|
|
|
"A tag < $tagid > não existe no XML!!" |
397
|
|
|
); |
398
|
|
|
} |
399
|
3 |
|
$this->docId = $node->getAttribute($marcador); |
400
|
3 |
|
$xmlResp = $xml; |
401
|
3 |
|
if (! $this->zSignatureExists($xmldoc)) { |
|
|
|
|
402
|
|
|
//executa a assinatura |
403
|
3 |
|
$xmlResp = $this->zSignXML($xmldoc, $root, $node, $objSSLPriKey, $marcador, $algorithm); |
|
|
|
|
404
|
3 |
|
} |
405
|
|
|
//libera a chave privada |
406
|
3 |
|
openssl_free_key($objSSLPriKey); |
407
|
3 |
|
return $xmlResp; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
/** |
411
|
|
|
* zSignXML |
412
|
|
|
* Método que provê a assinatura do xml conforme padrão SEFAZ |
413
|
|
|
* @param DOMDocument $xmldoc |
414
|
|
|
* @param DOMElement $root |
415
|
|
|
* @param DOMElement $node |
416
|
|
|
* @param resource $objSSLPriKey |
417
|
|
|
* @param string $marcador |
418
|
|
|
* @param string $algorithm |
419
|
|
|
* @return string xml assinado |
420
|
|
|
* @internal param DOMDocument $xmlDoc |
421
|
|
|
*/ |
422
|
3 |
|
private function zSignXML($xmldoc, $root, $node, $objSSLPriKey, $marcador, $algorithm = 'SHA1') |
423
|
|
|
{ |
424
|
3 |
|
$nsDSIG = 'http://www.w3.org/2000/09/xmldsig#'; |
425
|
3 |
|
$nsCannonMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; |
426
|
3 |
|
$nsSignatureMethod = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; |
427
|
3 |
|
$nsDigestMethod = 'http://www.w3.org/2000/09/xmldsig#sha1'; |
428
|
3 |
|
$signAlgorithm = OPENSSL_ALGO_SHA1; |
429
|
|
|
//incluido para atender requisitos de assinatura do sped-efinanceira |
430
|
3 |
|
if ($algorithm == 'SHA256') { |
431
|
|
|
$signAlgorithm = OPENSSL_ALGO_SHA256; |
432
|
|
|
$nsSignatureMethod = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; |
433
|
|
|
$nsDigestMethod = 'http://www.w3.org/2001/04/xmlenc#sha256'; |
434
|
|
|
} |
435
|
3 |
|
$nsTransformMethod1 ='http://www.w3.org/2000/09/xmldsig#enveloped-signature'; |
436
|
3 |
|
$nsTransformMethod2 = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; |
437
|
|
|
//pega o atributo id do node a ser assinado |
438
|
3 |
|
$idSigned = trim($node->getAttribute($marcador)); |
439
|
|
|
//extrai os dados da tag para uma string na forma canonica |
440
|
3 |
|
$dados = $node->C14N(true, false, null, null); |
441
|
|
|
//calcular o hash dos dados |
442
|
3 |
|
if ($algorithm == 'SHA256') { |
443
|
|
|
$hashValue = hash('sha256', $dados, true); |
444
|
|
|
} else { |
445
|
3 |
|
$hashValue = hash('sha1', $dados, true); |
446
|
|
|
} |
447
|
|
|
//converter o hash para base64 |
448
|
3 |
|
$digValue = base64_encode($hashValue); |
449
|
|
|
//cria o node <Signature> |
450
|
3 |
|
$signatureNode = $xmldoc->createElementNS($nsDSIG, 'Signature'); |
451
|
|
|
//adiciona a tag <Signature> ao node raiz |
452
|
3 |
|
$root->appendChild($signatureNode); |
453
|
|
|
//cria o node <SignedInfo> |
454
|
3 |
|
$signedInfoNode = $xmldoc->createElement('SignedInfo'); |
455
|
|
|
//adiciona o node <SignedInfo> ao <Signature> |
456
|
3 |
|
$signatureNode->appendChild($signedInfoNode); |
457
|
|
|
//cria no node com o método de canonização dos dados |
458
|
3 |
|
$canonicalNode = $xmldoc->createElement('CanonicalizationMethod'); |
459
|
|
|
//adiona o <CanonicalizationMethod> ao node <SignedInfo> |
460
|
3 |
|
$signedInfoNode->appendChild($canonicalNode); |
461
|
|
|
//seta o atributo ao node <CanonicalizationMethod> |
462
|
3 |
|
$canonicalNode->setAttribute('Algorithm', $nsCannonMethod); |
463
|
|
|
//cria o node <SignatureMethod> |
464
|
3 |
|
$signatureMethodNode = $xmldoc->createElement('SignatureMethod'); |
465
|
|
|
//adiciona o node <SignatureMethod> ao node <SignedInfo> |
466
|
3 |
|
$signedInfoNode->appendChild($signatureMethodNode); |
467
|
|
|
//seta o atributo Algorithm ao node <SignatureMethod> |
468
|
3 |
|
$signatureMethodNode->setAttribute('Algorithm', $nsSignatureMethod); |
469
|
|
|
//cria o node <Reference> |
470
|
3 |
|
$referenceNode = $xmldoc->createElement('Reference'); |
471
|
|
|
//adiciona o node <Reference> ao node <SignedInfo> |
472
|
3 |
|
$signedInfoNode->appendChild($referenceNode); |
473
|
|
|
//seta o atributo URI a node <Reference> |
474
|
3 |
|
$referenceNode->setAttribute('URI', '#'.$idSigned); |
475
|
|
|
//cria o node <Transforms> |
476
|
3 |
|
$transformsNode = $xmldoc->createElement('Transforms'); |
477
|
|
|
//adiciona o node <Transforms> ao node <Reference> |
478
|
3 |
|
$referenceNode->appendChild($transformsNode); |
479
|
|
|
//cria o primeiro node <Transform> OBS: no singular |
480
|
3 |
|
$transfNode1 = $xmldoc->createElement('Transform'); |
481
|
|
|
//adiciona o primeiro node <Transform> ao node <Transforms> |
482
|
3 |
|
$transformsNode->appendChild($transfNode1); |
483
|
|
|
//set o atributo Algorithm ao primeiro node <Transform> |
484
|
3 |
|
$transfNode1->setAttribute('Algorithm', $nsTransformMethod1); |
485
|
|
|
//cria outro node <Transform> OBS: no singular |
486
|
3 |
|
$transfNode2 = $xmldoc->createElement('Transform'); |
487
|
|
|
//adiciona o segundo node <Transform> ao node <Transforms> |
488
|
3 |
|
$transformsNode->appendChild($transfNode2); |
489
|
|
|
//set o atributo Algorithm ao segundo node <Transform> |
490
|
3 |
|
$transfNode2->setAttribute('Algorithm', $nsTransformMethod2); |
491
|
|
|
//cria o node <DigestMethod> |
492
|
3 |
|
$digestMethodNode = $xmldoc->createElement('DigestMethod'); |
493
|
|
|
//adiciona o node <DigestMethod> ao node <Reference> |
494
|
3 |
|
$referenceNode->appendChild($digestMethodNode); |
495
|
|
|
//seta o atributo Algorithm ao node <DigestMethod> |
496
|
3 |
|
$digestMethodNode->setAttribute('Algorithm', $nsDigestMethod); |
497
|
|
|
//cria o node <DigestValue> |
498
|
3 |
|
$digestValueNode = $xmldoc->createElement('DigestValue', $digValue); |
499
|
|
|
//adiciona o node <DigestValue> ao node <Reference> |
500
|
3 |
|
$referenceNode->appendChild($digestValueNode); |
501
|
|
|
//extrai node <SignedInfo> para uma string na sua forma canonica |
502
|
3 |
|
$cnSignedInfoNode = $signedInfoNode->C14N(true, false, null, null); |
503
|
|
|
//cria uma variavel vazia que receberá a assinatura |
504
|
3 |
|
$signature = ''; |
505
|
|
|
//calcula a assinatura do node canonizado <SignedInfo> |
506
|
|
|
//usando a chave privada em formato PEM |
507
|
3 |
|
if (! openssl_sign($cnSignedInfoNode, $signature, $objSSLPriKey, $signAlgorithm)) { |
508
|
|
|
$msg = "Houve erro durante a assinatura digital.\n"; |
509
|
|
|
$this->zGetOpenSSLError($msg); |
510
|
|
|
//while ($erro = openssl_error_string()) { |
|
|
|
|
511
|
|
|
// $msg .= $erro . "\n"; |
512
|
|
|
//} |
513
|
|
|
//throw new Exception\RuntimeException($msg); |
|
|
|
|
514
|
|
|
} |
515
|
|
|
//converte a assinatura em base64 |
516
|
3 |
|
$signatureValue = base64_encode($signature); |
517
|
|
|
//cria o node <SignatureValue> |
518
|
3 |
|
$signatureValueNode = $xmldoc->createElement('SignatureValue', $signatureValue); |
519
|
|
|
//adiciona o node <SignatureValue> ao node <Signature> |
520
|
3 |
|
$signatureNode->appendChild($signatureValueNode); |
521
|
|
|
//cria o node <KeyInfo> |
522
|
3 |
|
$keyInfoNode = $xmldoc->createElement('KeyInfo'); |
523
|
|
|
//adiciona o node <KeyInfo> ao node <Signature> |
524
|
3 |
|
$signatureNode->appendChild($keyInfoNode); |
525
|
|
|
//cria o node <X509Data> |
526
|
3 |
|
$x509DataNode = $xmldoc->createElement('X509Data'); |
527
|
|
|
//adiciona o node <X509Data> ao node <KeyInfo> |
528
|
3 |
|
$keyInfoNode->appendChild($x509DataNode); |
529
|
|
|
//remove linhas desnecessárias do certificado |
530
|
3 |
|
$pubKeyClean = $this->zCleanPubKey(); |
531
|
|
|
//cria o node <X509Certificate> |
532
|
3 |
|
$x509CertificateNode = $xmldoc->createElement('X509Certificate', $pubKeyClean); |
533
|
|
|
//adiciona o node <X509Certificate> ao node <X509Data> |
534
|
3 |
|
$x509DataNode->appendChild($x509CertificateNode); |
535
|
|
|
//salva o xml completo em uma string |
536
|
3 |
|
$xmlResp = $xmldoc->saveXML(); |
537
|
|
|
//retorna o documento assinado |
538
|
3 |
|
return $xmlResp; |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* signatureExists |
543
|
|
|
* Check se o xml possi a tag Signature |
544
|
|
|
* @param DOMDocument $dom |
545
|
|
|
* @return boolean |
546
|
|
|
*/ |
547
|
6 |
|
private function zSignatureExists($dom) |
548
|
|
|
{ |
549
|
6 |
|
$signature = $dom->getElementsByTagName('Signature')->item(0); |
550
|
6 |
|
if (! isset($signature)) { |
551
|
3 |
|
return false; |
552
|
|
|
} |
553
|
3 |
|
return true; |
554
|
|
|
} |
555
|
|
|
|
556
|
|
|
/** |
557
|
|
|
* verifySignature |
558
|
|
|
* Verifica a validade da assinatura digital contida no xml |
559
|
|
|
* @param string $docxml conteudo do xml a ser verificado ou o path completo |
560
|
|
|
* @param string $tagid tag que foi assinada no documento xml |
561
|
|
|
* @return boolean |
562
|
|
|
* @throws Exception\InvalidArgumentException |
563
|
|
|
* @throws Exception\RuntimeException |
564
|
|
|
*/ |
565
|
3 |
|
public function verifySignature($docxml = '', $tagid = '') |
566
|
|
|
{ |
567
|
3 |
|
if ($docxml == '') { |
568
|
|
|
$msg = "Não foi passado um xml para a verificação."; |
569
|
|
|
throw new Exception\InvalidArgumentException($msg); |
570
|
|
|
} |
571
|
3 |
|
if ($tagid == '') { |
572
|
|
|
$msg = "Não foi indicada a TAG a ser verificada."; |
573
|
|
|
throw new Exception\InvalidArgumentException($msg); |
574
|
|
|
} |
575
|
3 |
|
$xml = $docxml; |
576
|
3 |
|
if (is_file($docxml)) { |
577
|
3 |
|
$xml = file_get_contents($docxml); |
578
|
3 |
|
} |
579
|
3 |
|
$dom = new Dom(); |
580
|
3 |
|
$dom->loadXMLString($xml); |
581
|
3 |
|
$flag = $this->zDigCheck($dom, $tagid); |
|
|
|
|
582
|
3 |
|
$flag = $this->zSignCheck($dom); |
|
|
|
|
583
|
3 |
|
return $flag; |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
/** |
587
|
|
|
* zSignCheck |
588
|
|
|
* @param DOMDocument $dom |
589
|
|
|
* @return boolean |
590
|
|
|
* @throws Exception\RuntimeException |
591
|
|
|
*/ |
592
|
3 |
|
private function zSignCheck($dom) |
593
|
|
|
{ |
594
|
|
|
//SignatureMethod attribute Algorithm |
595
|
|
|
//<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/> |
596
|
3 |
|
$sigMethAlgo = $dom->getNode('SignatureMethod', 0)->getAttribute('Algorithm'); |
597
|
3 |
|
if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') { |
598
|
3 |
|
$signAlgorithm = OPENSSL_ALGO_SHA1; |
599
|
3 |
|
} else { |
600
|
|
|
$signAlgorithm = OPENSSL_ALGO_SHA256; |
601
|
|
|
} |
602
|
|
|
// Obter e remontar a chave publica do xml |
603
|
3 |
|
$x509Certificate = $dom->getNodeValue('X509Certificate'); |
604
|
|
|
$x509Certificate = "-----BEGIN CERTIFICATE-----\n" |
605
|
3 |
|
. $this->zSplitLines($x509Certificate) |
606
|
3 |
|
. "\n-----END CERTIFICATE-----\n"; |
607
|
|
|
//carregar a chave publica remontada |
608
|
3 |
|
$objSSLPubKey = openssl_pkey_get_public($x509Certificate); |
609
|
3 |
|
if ($objSSLPubKey === false) { |
610
|
|
|
$msg = "Ocorreram problemas ao carregar a chave pública. Certificado incorreto ou corrompido!!"; |
611
|
|
|
$this->zGetOpenSSLError($msg); |
612
|
|
|
} |
613
|
|
|
//remontando conteudo que foi assinado |
614
|
3 |
|
$signContent = $dom->getElementsByTagName('SignedInfo')->item(0)->C14N(true, false, null, null); |
615
|
|
|
// validando assinatura do conteudo |
616
|
3 |
|
$signatureValueXML = $dom->getElementsByTagName('SignatureValue')->item(0)->nodeValue; |
617
|
3 |
|
$decodedSignature = base64_decode(str_replace(array("\r", "\n"), '', $signatureValueXML)); |
618
|
3 |
|
$resp = openssl_verify($signContent, $decodedSignature, $objSSLPubKey, $signAlgorithm); |
619
|
3 |
|
if ($resp != 1) { |
620
|
|
|
$msg = "Problema ({$resp}) ao verificar a assinatura do digital!!"; |
621
|
|
|
$this->zGetOpenSSLError($msg); |
622
|
|
|
} |
623
|
3 |
|
return true; |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
/** |
627
|
|
|
* zDigCheck |
628
|
|
|
* @param DOMDocument $dom |
629
|
|
|
* @param string $tagid |
630
|
|
|
* @return boolean |
631
|
|
|
* @throws Exception\RuntimeException |
632
|
|
|
*/ |
633
|
3 |
|
private function zDigCheck($dom, $tagid = '') |
634
|
|
|
{ |
635
|
3 |
|
$node = $dom->getNode($tagid, 0); |
636
|
3 |
|
if (empty($node)) { |
637
|
|
|
throw new Exception\RuntimeException( |
638
|
|
|
"A tag < $tagid > não existe no XML!!" |
639
|
|
|
); |
640
|
|
|
} |
641
|
3 |
|
if (! $this->zSignatureExists($dom)) { |
642
|
|
|
$msg = "O xml não contêm nenhuma assinatura para ser verificada."; |
643
|
|
|
throw new Exception\RuntimeException($msg); |
644
|
|
|
} |
645
|
|
|
//carregar o node em sua forma canonica |
646
|
3 |
|
$tagInf = $node->C14N(true, false, null, null); |
647
|
|
|
//calcular o hash sha1 |
648
|
3 |
|
$hashValue = hash('sha1', $tagInf, true); |
649
|
|
|
//converter o hash para base64 para obter o digest do node |
650
|
3 |
|
$digestCalculado = base64_encode($hashValue); |
651
|
|
|
//pegar o digest informado no xml |
652
|
3 |
|
$digestInformado = $dom->getNodeValue('DigestValue'); |
653
|
|
|
//compara os digests calculados e informados |
654
|
3 |
|
if ($digestCalculado != $digestInformado) { |
655
|
|
|
$msg = "O conteúdo do XML não confere com o Digest Value.\n |
656
|
|
|
Digest calculado [{$digestCalculado}], digest informado no XML [{$digestInformado}].\n |
657
|
|
|
O arquivo pode estar corrompido ou ter sido adulterado."; |
658
|
|
|
throw new Exception\RuntimeException($msg); |
659
|
|
|
} |
660
|
3 |
|
return true; |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
/** |
664
|
|
|
* zValidCerts |
665
|
|
|
* Verifica a data de validade do certificado digital |
666
|
|
|
* e compara com a data de hoje. |
667
|
|
|
* Caso o certificado tenha expirado o mesmo será removido das |
668
|
|
|
* pastas e o método irá retornar false. |
669
|
|
|
* @param string $pubKey chave publica |
670
|
|
|
* @return boolean |
671
|
|
|
*/ |
672
|
3 |
|
protected function zValidCerts($pubKey) |
673
|
|
|
{ |
674
|
3 |
|
if (! $data = openssl_x509_read($pubKey)) { |
675
|
|
|
//o dado não é uma chave válida |
676
|
|
|
$this->zRemovePemFiles(); |
677
|
|
|
$this->zLeaveParam(); |
678
|
|
|
$this->error = "A chave passada está corrompida ou não é uma chave. Obtenha s chaves corretas!!"; |
679
|
|
|
return false; |
680
|
|
|
} |
681
|
3 |
|
$certData = openssl_x509_parse($data); |
682
|
|
|
// reformata a data de validade; |
683
|
3 |
|
$ano = substr($certData['validTo'], 0, 2); |
684
|
3 |
|
$mes = substr($certData['validTo'], 2, 2); |
685
|
3 |
|
$dia = substr($certData['validTo'], 4, 2); |
686
|
|
|
//obtem o timestamp da data de validade do certificado |
687
|
3 |
|
$dValid = gmmktime(0, 0, 0, $mes, $dia, $ano); |
688
|
|
|
// obtem o timestamp da data de hoje |
689
|
3 |
|
$dHoje = gmmktime(0, 0, 0, date("m"), date("d"), date("Y")); |
690
|
|
|
// compara a data de validade com a data atual |
691
|
3 |
|
$this->expireTimestamp = $dValid; |
|
|
|
|
692
|
3 |
|
if ($dHoje > $dValid) { |
693
|
3 |
|
$this->zRemovePemFiles(); |
694
|
3 |
|
$this->zLeaveParam(); |
695
|
3 |
|
$msg = "Data de validade vencida! [Valido até $dia/$mes/$ano]"; |
696
|
3 |
|
$this->error = $msg; |
697
|
3 |
|
return false; |
698
|
|
|
} |
699
|
|
|
return true; |
700
|
|
|
} |
701
|
|
|
|
702
|
|
|
/** |
703
|
|
|
* zCleanPubKey |
704
|
|
|
* Remove a informação de inicio e fim do certificado |
705
|
|
|
* contido no formato PEM, deixando o certificado (chave publica) pronta para ser |
706
|
|
|
* anexada ao xml da NFe |
707
|
|
|
* @return string contendo o certificado limpo |
708
|
|
|
*/ |
709
|
3 |
|
protected function zCleanPubKey() |
710
|
|
|
{ |
711
|
|
|
//inicializa variavel |
712
|
3 |
|
$data = ''; |
713
|
|
|
//carregar a chave publica |
714
|
3 |
|
$pubKey = $this->pubKey; |
715
|
|
|
//carrega o certificado em um array usando o LF como referencia |
716
|
3 |
|
$arCert = explode("\n", $pubKey); |
717
|
3 |
|
foreach ($arCert as $curData) { |
718
|
|
|
//remove a tag de inicio e fim do certificado |
719
|
3 |
|
if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) != 0 && |
720
|
3 |
|
strncmp($curData, '-----END CERTIFICATE', 20) != 0 ) { |
721
|
|
|
//carrega o resultado numa string |
722
|
3 |
|
$data .= trim($curData); |
723
|
3 |
|
} |
724
|
3 |
|
} |
725
|
3 |
|
return $data; |
726
|
|
|
} |
727
|
|
|
|
728
|
|
|
/** |
729
|
|
|
* zSplitLines |
730
|
|
|
* Divide a string do certificado publico em linhas |
731
|
|
|
* com 76 caracteres (padrão original) |
732
|
|
|
* @param string $cntIn certificado |
733
|
|
|
* @return string certificado reformatado |
734
|
|
|
*/ |
735
|
3 |
|
protected function zSplitLines($cntIn = '') |
736
|
|
|
{ |
737
|
3 |
|
if ($cntIn != '') { |
738
|
3 |
|
$cnt = rtrim(chunk_split(str_replace(array("\r", "\n"), '', $cntIn), 76, "\n")); |
739
|
3 |
|
} else { |
740
|
|
|
$cnt = $cntIn; |
741
|
|
|
} |
742
|
3 |
|
return $cnt; |
743
|
|
|
} |
744
|
|
|
|
745
|
|
|
/** |
746
|
|
|
* zRemovePemFiles |
747
|
|
|
* Apaga os arquivos PEM do diretório |
748
|
|
|
* Isso deve ser feito quando um novo certificado é carregado |
749
|
|
|
* ou quando a validade do certificado expirou. |
750
|
|
|
*/ |
751
|
9 |
|
private function zRemovePemFiles() |
752
|
|
|
{ |
753
|
9 |
|
if (is_file($this->pubKeyFile)) { |
754
|
|
|
unlink($this->pubKeyFile); |
755
|
|
|
} |
756
|
9 |
|
if (is_file($this->priKeyFile)) { |
757
|
|
|
unlink($this->priKeyFile); |
758
|
|
|
} |
759
|
9 |
|
if (is_file($this->certKeyFile)) { |
760
|
|
|
unlink($this->certKeyFile); |
761
|
|
|
} |
762
|
9 |
|
} |
763
|
|
|
|
764
|
|
|
/** |
765
|
|
|
* zLeaveParam |
766
|
|
|
* Limpa os parametros da classe |
767
|
|
|
*/ |
768
|
3 |
|
private function zLeaveParam() |
769
|
|
|
{ |
770
|
3 |
|
$this->pfxCert=''; |
771
|
3 |
|
$this->pubKey=''; |
772
|
3 |
|
$this->priKey=''; |
773
|
3 |
|
$this->certKey=''; |
774
|
3 |
|
$this->pubKeyFile=''; |
775
|
3 |
|
$this->priKeyFile=''; |
776
|
3 |
|
$this->certKeyFile=''; |
777
|
3 |
|
$this->expireTimestamp=''; |
|
|
|
|
778
|
3 |
|
} |
779
|
|
|
|
780
|
|
|
/** |
781
|
|
|
* zGetOpenSSLError |
782
|
|
|
* @param string $msg |
783
|
|
|
* @return string |
784
|
|
|
*/ |
785
|
|
|
protected function zGetOpenSSLError($msg = '') |
786
|
|
|
{ |
787
|
|
|
while ($erro = openssl_error_string()) { |
788
|
|
|
$msg .= $erro . "\n"; |
789
|
|
|
} |
790
|
|
|
throw new Exception\RuntimeException($msg); |
791
|
|
|
} |
792
|
|
|
} |
793
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.