-
Notifications
You must be signed in to change notification settings - Fork 150
/
FirmaElectronica.php
530 lines (504 loc) · 20.7 KB
/
FirmaElectronica.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
<?php
/**
* LibreDTE
* Copyright (C) SASCO SpA (https://sasco.cl)
*
* Este programa es software libre: usted puede redistribuirlo y/o
* modificarlo bajo los términos de la Licencia Pública General Affero de GNU
* publicada por la Fundación para el Software Libre, ya sea la versión
* 3 de la Licencia, o (a su elección) cualquier versión posterior de la
* misma.
*
* Este programa se distribuye con la esperanza de que sea útil, pero
* SIN GARANTÍA ALGUNA; ni siquiera la garantía implícita
* MERCANTIL o de APTITUD PARA UN PROPÓSITO DETERMINADO.
* Consulte los detalles de la Licencia Pública General Affero de GNU para
* obtener una información más detallada.
*
* Debería haber recibido una copia de la Licencia Pública General Affero de GNU
* junto a este programa.
* En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
*/
namespace sasco\LibreDTE;
/**
* Clase para trabajar con firma electrónica, permite firmar y verificar firmas.
* Provee los métodos: sign(), verify(), signXML() y verifyXML()
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2019-02-12
*/
class FirmaElectronica
{
private $config; ///< Configuración de la firma electrónica
private $certs; ///< Certificados digitales de la firma
private $data; ///< Datos del certificado digial
/**
* Constructor para la clase: crea configuración y carga certificado digital
*
* Si se desea pasar una configuración específica para la firma electrónica
* se debe hacer a través de un arreglo con los índices file y pass, donde
* file es la ruta hacia el archivo .p12 que contiene tanto la clave privada
* como la pública y pass es la contraseña para abrir dicho archivo.
* Ejemplo:
*
* \code{.php}
* $firma_config = ['file'=>'/ruta/al/certificado.p12', 'pass'=>'contraseña'];
* $firma = new \sasco\LibreDTE\FirmaElectronica($firma_config);
* \endcode
*
* También se permite que en vez de pasar la ruta al certificado p12 se pase
* el contenido del certificado, esto servirá por ejemplo si los datos del
* archivo están almacenados en una base de datos. Ejemplo:
*
* \code{.php}
* $firma_config = ['data'=>file_get_contents('/ruta/al/certificado.p12'), 'pass'=>'contraseña'];
* $firma = new \sasco\LibreDTE\FirmaElectronica($firma_config);
* \endcode
*
* @param config Configuración para la clase, si no se especifica se tratará de determinar
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2019-12-04
*/
public function __construct(array $config = [])
{
// crear configuración
if (!$config) {
if (class_exists('\sowerphp\core\Configure')) {
$config = (array)\sowerphp\core\Configure::read('firma_electronica.default');
} else {
$config = [];
}
}
$this->config = array_merge([
'file' => null,
'pass' => null,
'wordwrap' => 64,
], $config);
// leer datos de la firma electrónica desde configuración con índices: cert y pkey
if (!empty($this->config['cert']) and !empty($this->config['pkey'])) {
$this->certs = [
'cert' => $this->config['cert'],
'pkey' => $this->config['pkey'],
];
unset($this->config['cert'], $this->config['pkey']);
}
// se pasó el archivo de la firma o bien los datos de la firma
else {
// cargar firma electrónica desde el contenido del archivo .p12 si no
// se pasaron como datos del arreglo de configuración
if (empty($this->config['data']) and $this->config['file']) {
if (is_readable($this->config['file'])) {
$this->config['data'] = file_get_contents($this->config['file']);
} else {
return $this->error('Archivo de la firma electrónica '.basename($this->config['file']).' no puede ser leído');
}
}
// leer datos de la firma desde el archivo que se ha pasado
if (!empty($this->config['data'])) {
if (openssl_pkcs12_read($this->config['data'], $this->certs, $this->config['pass'])===false) {
return $this->error('No fue posible leer los datos de la firma electrónica (verificar la contraseña)');
}
unset($this->config['data']);
}
}
$this->data = openssl_x509_parse($this->certs['cert']);
}
/**
* Método para generar un error usando una excepción de SowerPHP o terminar
* el script si no se está usando el framework
* @param msg Mensaje del error
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2017-08-04
*/
private function error($msg)
{
if (class_exists('\sasco\LibreDTE\Estado') and class_exists('\sasco\LibreDTE\Log')) {
$msg = \sasco\LibreDTE\Estado::get(\sasco\LibreDTE\Estado::FIRMA_ERROR, $msg);
\sasco\LibreDTE\Log::write(\sasco\LibreDTE\Estado::FIRMA_ERROR, $msg);
return false;
} else {
throw new \Exception($msg);
}
}
/**
* Método que agrega el inicio y fin de un certificado (clave pública)
* @param cert Certificado que se desea normalizar
* @return Certificado con el inicio y fin correspondiente
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2015-08-20
*/
private function normalizeCert($cert)
{
if (strpos($cert, '-----BEGIN CERTIFICATE-----')===false) {
$body = trim($cert);
$cert = '-----BEGIN CERTIFICATE-----'."\n";
$cert .= wordwrap($body, $this->config['wordwrap'], "\n", true)."\n";
$cert .= '-----END CERTIFICATE-----'."\n";
}
return $cert;
}
/**
* Método que entrega el RUN/RUT asociado al certificado
* @return RUN/RUT asociado al certificado en formato: 11222333-4
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2016-02-12
*/
public function getID()
{
// RUN/RUT se encuentra en la extensión del certificado, esto de acuerdo
// a Ley 19.799 sobre documentos electrónicos y firma electrónica
$x509 = new \phpseclib\File\X509();
$cert = $x509->loadX509($this->certs['cert']);
if (isset($cert['tbsCertificate']['extensions'])) {
foreach ($cert['tbsCertificate']['extensions'] as $e) {
if ($e['extnId']=='id-ce-subjectAltName') {
return ltrim($e['extnValue'][0]['otherName']['value']['ia5String'], '0');
}
}
}
// se obtiene desde serialNumber (esto es sólo para que funcione la firma para tests)
if (isset($this->data['subject']['serialNumber'])) {
return ltrim($this->data['subject']['serialNumber'], '0');
}
// no se encontró el RUN
return $this->error('No fue posible obtener el ID de la firma');
}
/**
* Método que entrega el CN del subject
* @return CN del subject
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2016-02-12
*/
public function getName()
{
if (isset($this->data['subject']['CN']))
return $this->data['subject']['CN'];
return $this->error('No fue posible obtener el Name (subject.CN) de la firma');
}
/**
* Método que entrega el emailAddress del subject
* @return emailAddress del subject
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2016-02-12
*/
public function getEmail()
{
if (isset($this->data['subject']['emailAddress'])) {
return $this->data['subject']['emailAddress'];
}
return $this->error('No fue posible obtener el Email (subject.emailAddress) de la firma');
}
/**
* Método que entrega desde cuando es válida la firma
* @return validFrom_time_t
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2015-09-22
*/
public function getFrom()
{
return date('Y-m-d H:i:s', $this->data['validFrom_time_t']);
}
/**
* Método que entrega hasta cuando es válida la firma
* @return validTo_time_t
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2015-09-22
*/
public function getTo()
{
return date('Y-m-d H:i:s', $this->data['validTo_time_t']);
}
/**
* Método que entrega los días totales que la firma es válida
* @return int Días totales en que la firma es válida
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2019-02-12
*/
public function getTotalDays()
{
$start = new \DateTime($this->getFrom());
$end = new \DateTime($this->getTo());
$diff = $start->diff($end);
return $diff->format('%a');
}
/**
* Método que entrega los días que faltan para que la firma expire
* @return int Días que faltan para que la firma expire
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2019-02-12
*/
public function getExpirationDays($desde = null)
{
if (!$desde) {
$desde = date('Y-m-d H:i:s');
}
$start = new \DateTime($desde);
$end = new \DateTime($this->getTo());
$diff = $start->diff($end);
return $diff->format('%a');
}
/**
* Método que entrega el nombre del emisor de la firma
* @return CN del issuer
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2015-09-22
*/
public function getIssuer()
{
return $this->data['issuer']['CN'];
}
/**
* Método que entrega los datos del certificado
* @return Arreglo con todo los datos del certificado
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2015-09-11
*/
public function getData()
{
return $this->data;
}
/**
* Método que obtiene el módulo de la clave privada
* @return Módulo en base64
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2014-12-07
*/
public function getModulus()
{
$details = openssl_pkey_get_details(openssl_pkey_get_private($this->certs['pkey']));
return wordwrap(base64_encode($details['rsa']['n']), $this->config['wordwrap'], "\n", true);
}
/**
* Método que obtiene el exponente público de la clave privada
* @return Exponente público en base64
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2014-12-06
*/
public function getExponent()
{
$details = openssl_pkey_get_details(openssl_pkey_get_private($this->certs['pkey']));
return wordwrap(base64_encode($details['rsa']['e']), $this->config['wordwrap'], "\n", true);
}
/**
* Método que entrega el certificado de la firma
* @return Contenido del certificado, clave pública del certificado digital, en base64
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2015-08-24
*/
public function getCertificate($clean = false)
{
if ($clean) {
return trim(str_replace(
['-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----'],
'',
$this->certs['cert']
));
} else {
return $this->certs['cert'];
}
}
/**
* Método que entrega la clave privada de la firma
* @return Contenido de la clave privada del certificado digital en base64
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2015-08-24
*/
public function getPrivateKey($clean = false)
{
if ($clean) {
return trim(str_replace(
['-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----'],
'',
$this->certs['pkey']
));
} else {
return $this->certs['pkey'];
}
}
/**
* Método para realizar la firma de datos
* @param data Datos que se desean firmar
* @param signature_alg Algoritmo que se utilizará para firmar (por defect SHA1)
* @return Firma digital de los datos en base64 o =false si no se pudo firmar
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2014-12-08
*/
public function sign($data, $signature_alg = OPENSSL_ALGO_SHA1)
{
$signature = null;
if (openssl_sign($data, $signature, $this->certs['pkey'], $signature_alg)==false) {
return $this->error('No fue posible firmar los datos');
}
return base64_encode($signature);
}
/**
* Método que verifica la firma digital de datos
* @param data Datos que se desean verificar
* @param signature Firma digital de los datos en base64
* @param pub_key Certificado digital, clave pública, de la firma
* @param signature_alg Algoritmo que se usó para firmar (por defect SHA1)
* @return =true si la firma está ok, =false si está mal o no se pudo determinar
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2014-12-08
*/
public function verify($data, $signature, $pub_key = null, $signature_alg = OPENSSL_ALGO_SHA1)
{
if ($pub_key === null)
$pub_key = $this->certs['cert'];
$pub_key = $this->normalizeCert($pub_key);
return openssl_verify($data, base64_decode($signature), $pub_key, $signature_alg) == 1 ? true : false;
}
/**
* Método que firma un XML utilizando RSA y SHA1
*
* Referencia: http://www.di-mgt.com.au/xmldsig2.html
*
* @param xml Datos XML que se desean firmar
* @param reference Referencia a la que hace la firma
* @return XML firmado o =false si no se pudo fimar
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2017-10-22
*/
public function signXML($xml, $reference = '', $tag = null, $xmlns_xsi = false)
{
// normalizar 4to parámetro que puede ser boolean o array
if (is_array($xmlns_xsi)) {
$namespace = $xmlns_xsi;
$xmlns_xsi = false;
} else {
$namespace = null;
}
// obtener objeto del XML que se va a firmar
$doc = new XML();
$doc->loadXML($xml);
if (!$doc->documentElement) {
return $this->error('No se pudo obtener el documentElement desde el XML a firmar (posible XML mal formado)');
}
// crear nodo para la firma
$Signature = $doc->importNode((new XML())->generate([
'Signature' => [
'@attributes' => $namespace ? false : [
'xmlns' => 'http://www.w3.org/2000/09/xmldsig#',
],
'SignedInfo' => [
'@attributes' => $namespace ? false : [
'xmlns' => 'http://www.w3.org/2000/09/xmldsig#',
'xmlns:xsi' => $xmlns_xsi ? 'http://www.w3.org/2001/XMLSchema-instance' : false,
],
'CanonicalizationMethod' => [
'@attributes' => [
'Algorithm' => 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315',
],
],
'SignatureMethod' => [
'@attributes' => [
'Algorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
],
],
'Reference' => [
'@attributes' => [
'URI' => $reference,
],
'Transforms' => [
'Transform' => [
'@attributes' => [
'Algorithm' => $namespace ? 'http://www.altova.com' : 'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
],
],
],
'DigestMethod' => [
'@attributes' => [
'Algorithm' => 'http://www.w3.org/2000/09/xmldsig#sha1',
],
],
'DigestValue' => null,
],
],
'SignatureValue' => null,
'KeyInfo' => [
'KeyValue' => [
'RSAKeyValue' => [
'Modulus' => null,
'Exponent' => null,
],
],
'X509Data' => [
'X509Certificate' => null,
],
],
],
], $namespace)->documentElement, true);
// calcular DigestValue
if ($tag) {
$item = $doc->documentElement->getElementsByTagName($tag)->item(0);
if (!$item) {
return $this->error('No fue posible obtener el nodo con el tag '.$tag);
}
$digest = base64_encode(sha1($item->C14N(), true));
} else {
$digest = base64_encode(sha1($doc->C14N(), true));
}
$Signature->getElementsByTagName('DigestValue')->item(0)->nodeValue = $digest;
// calcular SignatureValue
$SignedInfo = $doc->saveHTML($Signature->getElementsByTagName('SignedInfo')->item(0));
$firma = $this->sign($SignedInfo);
if (!$firma)
return false;
$signature = wordwrap($firma, $this->config['wordwrap'], "\n", true);
// reemplazar valores en la firma de
$Signature->getElementsByTagName('SignatureValue')->item(0)->nodeValue = $signature;
$Signature->getElementsByTagName('Modulus')->item(0)->nodeValue = $this->getModulus();
$Signature->getElementsByTagName('Exponent')->item(0)->nodeValue = $this->getExponent();
$Signature->getElementsByTagName('X509Certificate')->item(0)->nodeValue = $this->getCertificate(true);
// agregar y entregar firma
$doc->documentElement->appendChild($Signature);
return $doc->saveXML();
}
/**
* Método que verifica la validez de la firma de un XML utilizando RSA y SHA1
* @param xml_data Archivo XML que se desea validar
* @return =true si la firma del documento XML es válida o =false si no lo es
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2015-09-02
*/
public function verifyXML($xml_data, $tag = null)
{
$doc = new XML();
$doc->loadXML($xml_data);
// preparar datos que se verificarán
$SignaturesElements = $doc->documentElement->getElementsByTagName('Signature');
$Signature = $doc->documentElement->removeChild($SignaturesElements->item($SignaturesElements->length-1));
$SignedInfo = $Signature->getElementsByTagName('SignedInfo')->item(0);
$SignedInfo->setAttribute('xmlns', $Signature->getAttribute('xmlns'));
$signed_info = $doc->saveHTML($SignedInfo);
$signature = $Signature->getElementsByTagName('SignatureValue')->item(0)->nodeValue;
$pub_key = $Signature->getElementsByTagName('X509Certificate')->item(0)->nodeValue;
// verificar firma
if (!$this->verify($signed_info, $signature, $pub_key))
return false;
// verificar digest
$digest_original = $Signature->getElementsByTagName('DigestValue')->item(0)->nodeValue;
if ($tag) {
$digest_calculado = base64_encode(sha1($doc->documentElement->getElementsByTagName($tag)->item(0)->C14N(), true));
} else {
$digest_calculado = base64_encode(sha1($doc->C14N(), true));
}
return $digest_original == $digest_calculado;
}
/**
* Método que obtiene la clave asociada al módulo y exponente entregados
* @param modulus Módulo de la clave
* @param exponent Exponente de la clave
* @return Entrega la clave asociada al módulo y exponente
* @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
* @version 2015-09-19
*/
public static function getFromModulusExponent($modulus, $exponent)
{
$rsa = new \phpseclib\Crypt\RSA();
$modulus = new \phpseclib\Math\BigInteger(base64_decode($modulus), 256);
$exponent = new \phpseclib\Math\BigInteger(base64_decode($exponent), 256);
$rsa->loadKey(['n' => $modulus, 'e' => $exponent]);
$rsa->setPublicKey();
return $rsa->getPublicKey();
}
}