GeneratorWorker   A
last analyzed

Complexity

Total Complexity 13

Size/Duplication

Total Lines 203
Duplicated Lines 0 %

Test Coverage

Coverage 87.8%

Importance

Changes 0
Metric Value
eloc 69
c 0
b 0
f 0
dl 0
loc 203
ccs 72
cts 82
cp 0.878
rs 10
wmc 13

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A digestXmlReference() 0 30 3
A createSignatureNodeXml() 0 13 1
A signXml() 0 46 3
A signSignature() 0 41 3
A sign() 0 21 2
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Derafu: Biblioteca PHP (Núcleo).
7
 * Copyright (C) Derafu <https://www.derafu.org>
8
 *
9
 * Este programa es software libre: usted puede redistribuirlo y/o modificarlo
10
 * bajo los términos de la Licencia Pública General Affero de GNU publicada por
11
 * la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o
12
 * (a su elección) cualquier versión posterior de la misma.
13
 *
14
 * Este programa se distribuye con la esperanza de que sea útil, pero SIN
15
 * GARANTÍA ALGUNA; ni siquiera la garantía implícita MERCANTIL o de APTITUD
16
 * PARA UN PROPÓSITO DETERMINADO. Consulte los detalles de la Licencia Pública
17
 * General Affero de GNU para obtener una información más detallada.
18
 *
19
 * Debería haber recibido una copia de la Licencia Pública General Affero de GNU
20
 * junto a este programa.
21
 *
22
 * En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
23
 */
24
25
namespace Derafu\Lib\Core\Package\Prime\Component\Signature\Worker;
26
27
use Derafu\Lib\Core\Foundation\Abstract\AbstractWorker;
28
use Derafu\Lib\Core\Package\Prime\Component\Certificate\Contract\CertificateInterface;
29
use Derafu\Lib\Core\Package\Prime\Component\Signature\Contract\GeneratorWorkerInterface;
30
use Derafu\Lib\Core\Package\Prime\Component\Signature\Contract\SignatureInterface;
31
use Derafu\Lib\Core\Package\Prime\Component\Signature\Entity\Signature;
32
use Derafu\Lib\Core\Package\Prime\Component\Signature\Exception\SignatureException;
33
use Derafu\Lib\Core\Package\Prime\Component\Xml\Contract\XmlComponentInterface;
34
use Derafu\Lib\Core\Package\Prime\Component\Xml\Contract\XmlInterface;
35
use Derafu\Lib\Core\Package\Prime\Component\Xml\Entity\Xml;
36
use LogicException;
37
38
/**
39
 * Clase que maneja la generación de firmas electrónicas, en particular para
40
 * documentos XML.
41
 */
42
class GeneratorWorker extends AbstractWorker implements GeneratorWorkerInterface
43
{
44
    /**
45
     * Servicio de XMLs.
46
     *
47
     * @var XmlComponentInterface
48
     */
49
    private XmlComponentInterface $xmlComponent;
50
51
    /**
52
     * Constructor del generador de firmas electrónicas.
53
     *
54
     * @param XmlComponentInterface $xmlComponent
55
     */
56 6
    public function __construct(XmlComponentInterface $xmlComponent)
57
    {
58 6
        $this->xmlComponent = $xmlComponent;
59
    }
60
61
    /**
62
     * {@inheritDoc}
63
     */
64 3
    public function sign(
65
        string $data,
66
        string $privateKey,
67
        string|int $signatureAlgorithm = OPENSSL_ALGO_SHA1
68
    ): string {
69
        // Firmar los datos.
70 3
        $signature = null;
71 3
        $result = openssl_sign(
72 3
            $data,
73 3
            $signature,
74 3
            $privateKey,
75 3
            $signatureAlgorithm
76 3
        );
77
78
        // Si no se logró firmar los datos se lanza una excepción.
79 3
        if ($result === false) {
80
            throw new SignatureException('No fue posible firmar los datos.');
81
        }
82
83
        // Entregar la firma en base64.
84 3
        return base64_encode($signature);
85
    }
86
87
    /**
88
     * {@inheritDoc}
89
     */
90 4
    public function signXml(
91
        XmlInterface|string $xml,
92
        CertificateInterface $certificate,
93
        ?string $reference = null
94
    ): string {
95
        // Si se pasó un objeto Xml se convierte a string. Esto es
96
        // necesario para poder mantener el formato "lindo" si se pasó y poder
97
        // obtener el C14N de manera correcta.
98 4
        if (!is_string($xml)) {
99 3
            $xml = $xml->saveXml();
100
        }
101
102
        // Cargar el XML que se desea firmar.
103 4
        $doc = new Xml();
104 4
        $doc->loadXml($xml);
105 4
        if (!$doc->documentElement) {
106
            throw new SignatureException(
107
                'No se pudo obtener el documentElement desde el XML a firmar (posible XML mal formado).'
108
            );
109
        }
110
111
        // Calcular el "DigestValue" de los datos de la referencia.
112 4
        $digestValue = $this->digestXmlReference($doc, $reference);
113
114
        // Crear la instancia que representa el nodo de la firma con sus datos.
115 3
        $signatureNode = (new Signature())->configureSignatureData(
116 3
            reference: $reference,
117 3
            digestValue: $digestValue,
118 3
            certificate: $certificate
119 3
        );
120
121
        // Firmar el documento calculando el valor de la firma del nodo
122
        // `Signature`.
123 3
        $signatureNode = $this->signSignature(
124 3
            $signatureNode,
125 3
            $certificate
126 3
        );
127 3
        $xmlSignature = $signatureNode->getXml()->getXml();
128
129
        // Agregar la firma del XML en el nodo Signature.
130 3
        $signatureElement = $doc->createElement('Signature', '');
131 3
        $doc->documentElement->appendChild($signatureElement);
132 3
        $xmlSigned = str_replace('<Signature/>', $xmlSignature, $doc->saveXml());
133
134
        // Entregar el string XML del documento XML firmado.
135 3
        return $xmlSigned;
136
    }
137
138
    /**
139
     * {@inheritDoc}
140
     */
141 6
    public function digestXmlReference(
142
        XmlInterface $doc,
143
        ?string $reference = null
144
    ): string {
145
        // Se hará la digestión de una referencia (ID) específico en el XML.
146 6
        if (!empty($reference)) {
147 4
            $xpath = '//*[@ID="' . ltrim($reference, '#') . '"]';
148 4
            $dataToDigest = $doc->C14NWithIsoEncoding($xpath);
149
        }
150
        // Cuando no hay referencia, el digest es sobre todo el documento XML.
151
        // Si el XML ya tiene un nodo "Signature" dentro del nodo raíz se debe
152
        // eliminar ese nodo del XML antes de obtener su C14N.
153
        else {
154 2
            $docClone = clone $doc;
155 2
            $rootElement = $docClone->getDocumentElement();
156 2
            $signatureElement = $rootElement
157 2
                ->getElementsByTagName('Signature')
158 2
                ->item(0)
159 2
            ;
160 2
            if ($signatureElement) {
161 2
                $rootElement->removeChild($signatureElement);
162
            }
163 2
            $dataToDigest = $docClone->C14NWithIsoEncoding();
164
        }
165
166
        // Calcular la digestión sobre los datos del XML en formato C14N.
167 5
        $digestValue = base64_encode(sha1($dataToDigest, true));
168
169
        // Entregar el digest calculado.
170 5
        return $digestValue;
171
    }
172
173
    /**
174
     * Firma el nodo `SignedInfo` del documento XML utilizando un certificado
175
     * digital. Si no se ha proporcionado previamente un certificado, este
176
     * puede ser pasado como argumento en la firma.
177
     *
178
     * @return SignatureInterface El nodo de la firma que se firmará.
179
     * @param CertificateInterface $certificate Certificado digital a usar para firmar.
180
     * @return SignatureInterface El nodo de la firma firmado.
181
     * @throws LogicException Si no están las condiciones para firmar.
182
     */
183 3
    private function signSignature(
184
        SignatureInterface $signatureNode,
185
        CertificateInterface $certificate
186
    ): SignatureInterface {
187
        // Validar que esté asignado el DigestValue.
188 3
        if ($signatureNode->getDigestValue() === null) {
189
            throw new LogicException(
190
                'No es posible generar la firma del nodo Signature si aun no se asigna el DigestValue.'
191
            );
192
        }
193
194
        // Validar que esté asignado el certificado digital.
195 3
        if ($signatureNode->getX509Certificate() === null) {
196
            throw new LogicException(
197
                'No es posible generar la firma del nodo Signature si aun no se asigna el certificado digital.'
198
            );
199
        }
200
201
        // Crear el documento XML del nodo de la firma electrónica.
202 3
        $nodeXml = $this->createSignatureNodeXml(
203 3
            $signatureNode
204 3
        );
205
206
        // Generar el string XML de los datos que se firmarán.
207 3
        $xpath = "//*[local-name()='Signature']/*[local-name()='SignedInfo']";
208 3
        $signedInfoC14N = $nodeXml->C14NWithIsoEncoding($xpath);
209
210
        // Generar la firma de los datos, el tag `SignedInfo`.
211 3
        $signature = $this->sign(
212 3
            $signedInfoC14N,
213 3
            $certificate->getPrivateKey()
214 3
        );
215
216
        // Asignar la firma calculada al nodo de la firma.
217 3
        $signatureNode->setSignatureValue($signature);
218
219
        // Volver a crear el Xml del nodo de la firma.
220 3
        $this->createSignatureNodeXml($signatureNode);
221
222
        // Entregar el nodo de la firma.
223 3
        return $signatureNode;
224
    }
225
226
    /**
227
     * Crea la instancia Xml de Signature y la asigna a esta.
228
     *
229
     * @param SignatureInterface $signatureNode
230
     * @return XmlInterface
231
     */
232 3
    private function createSignatureNodeXml(
233
        SignatureInterface $signatureNode
234
    ): XmlInterface {
235 3
        $xml = new Xml();
236 3
        $xml->formatOutput = false;
237 3
        $xml = $this->xmlComponent->getEncoderWorker()->encode(
238 3
            data: $signatureNode->getData(),
239 3
            doc: $xml // Se pasa el Xml para asignar formatOutput.
240 3
        );
241
242 3
        $signatureNode->setXml($xml);
243
244 3
        return $signatureNode->getXml();
245
    }
246
}
247