Passed
Branch master (0b4ab1)
by Esteban De La Fuente
74:02 queued 50:02
created

Certificate   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 320
Duplicated Lines 0 %

Test Coverage

Coverage 63.73%

Importance

Changes 0
Metric Value
eloc 82
c 0
b 0
f 0
dl 0
loc 320
ccs 65
cts 102
cp 0.6373
rs 9.52
wmc 36

17 Methods

Rating   Name   Duplication   Size   Complexity  
A isActive() 0 11 4
A __construct() 0 4 1
A getEmail() 0 10 2
A getData() 0 7 2
A getModulus() 0 11 2
B getID() 0 30 8
A getFrom() 0 3 1
A getName() 0 10 2
A getIssuer() 0 3 1
A getPublicKey() 0 11 2
A getExponent() 0 11 2
A getTotalDays() 0 6 1
A getCertificate() 0 3 1
A getExpirationDays() 0 9 2
A getTo() 0 3 1
A getPrivateKey() 0 11 2
A getPrivateKeyDetails() 0 9 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\Certificate\Entity;
26
27
use DateTime;
28
use Derafu\Lib\Core\Helper\AsymmetricKey;
29
use Derafu\Lib\Core\Helper\Str;
30
use Derafu\Lib\Core\Package\Prime\Component\Certificate\Contract\CertificateInterface;
31
use Derafu\Lib\Core\Package\Prime\Component\Certificate\Exception\CertificateException;
32
use phpseclib3\File\X509;
33
34
/**
35
 * Clase que representa un certificado digital.
36
 */
37
class Certificate implements CertificateInterface
38
{
39
    /**
40
     * Clave pública (certificado).
41
     *
42
     * @var string
43
     */
44
    private string $publicKey;
45
46
    /**
47
     * Clave privada.
48
     *
49
     * @var string
50
     */
51
    private string $privateKey;
52
53
    /**
54
     * Detalles de la clave privada.
55
     *
56
     * @var array
57
     */
58
    private array $privateKeyDetails;
59
60
    /**
61
     * Datos parseados del certificado X509.
62
     *
63
     * @var array
64
     */
65
    private array $data;
66
67
    /**
68
     * Contructor del certificado digital.
69
     *
70
     * @param string $publicKey Clave pública (certificado).
71
     * @param string $privateKey Clave privada.
72
     */
73 20
    public function __construct(string $publicKey, string $privateKey)
74
    {
75 20
        $this->publicKey = AsymmetricKey::normalizePublicKey($publicKey);
76 20
        $this->privateKey = AsymmetricKey::normalizePrivateKey($privateKey);
77
    }
78
79
    /**
80
     * Entrega la clave pública (certificado) de la firma.
81
     *
82
     * @param bool $clean Si se limpia el contenido del certificado.
83
     * @return string Contenido del certificado, clave pública del certificado
84
     * digital, en base64.
85
     */
86 3
    public function getPublicKey(bool $clean = false): string
87
    {
88 3
        if ($clean) {
89 3
            return trim(str_replace(
90 3
                ['-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----'],
91 3
                '',
92 3
                $this->publicKey
93 3
            ));
94
        }
95
96
        return $this->publicKey;
97
    }
98
99
    /**
100
     * Entrega la clave pública (certificado) de la firma.
101
     *
102
     * @param bool $clean Si se limpia el contenido del certificado.
103
     * @return string Contenido del certificado, clave pública del certificado
104
     * digital, en base64.
105
     */
106 3
    public function getCertificate(bool $clean = false): string
107
    {
108 3
        return $this->getPublicKey($clean);
109
    }
110
111
    /**
112
     * Entrega la clave privada de la firma.
113
     *
114
     * @param bool $clean Si se limpia el contenido de la clave privada.
115
     * @return string Contenido de la clave privada del certificado digital
116
     * en base64.
117
     */
118 3
    public function getPrivateKey(bool $clean = false): string
119
    {
120 3
        if ($clean) {
121
            return trim(str_replace(
122
                ['-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----'],
123
                '',
124
                $this->privateKey
125
            ));
126
        }
127
128 3
        return $this->privateKey;
129
    }
130
131
    /**
132
     * Entrega los detalles de la llave privada.
133
     *
134
     * @return array
135
     */
136 5
    public function getPrivateKeyDetails(): array
137
    {
138 5
        if (!isset($this->privateKeyDetails)) {
139 5
            $this->privateKeyDetails = openssl_pkey_get_details(
140 5
                openssl_pkey_get_private($this->privateKey)
141 5
            );
142
        }
143
144 5
        return $this->privateKeyDetails;
145
    }
146
147
    /**
148
     * Entrega los datos del certificado.
149
     *
150
     * Alias de getCertX509().
151
     *
152
     * @return array Arreglo con todos los datos del certificado.
153
     */
154 9
    public function getData(): array
155
    {
156 9
        if (!isset($this->data)) {
157 9
            $this->data = openssl_x509_parse($this->publicKey);
158
        }
159
160 9
        return $this->data;
161
    }
162
163
    /**
164
     * Entrega el ID asociado al certificado.
165
     *
166
     * El ID es el RUN que debe estar en una extensión, esto es lo estándar.
167
     * También podría estar en el campo `serialNumber`, algunos proveedores lo
168
     * colocan en este campo, también es más fácil para pruebas
169
     *
170
     * @param bool $force_upper Si se fuerza a mayúsculas.
171
     * @return string ID asociado al certificado en formato: 11222333-4.
172
     */
173 6
    public function getID(bool $force_upper = true): string
174
    {
175
        // Verificar el serialNumber en el subject del certificado.
176 6
        $serialNumber = $this->getData()['subject']['serialNumber'] ?? null;
177 6
        if ($serialNumber !== null) {
178 6
            $serialNumber = ltrim(trim($serialNumber), '0');
179 6
            return $force_upper ? strtoupper($serialNumber) : $serialNumber;
180
        }
181
182
        // Obtener las extensiones del certificado.
183
        $x509 = new X509();
184
        $cert = $x509->loadX509($this->publicKey);
185
        if (isset($cert['tbsCertificate']['extensions'])) {
186
            foreach ($cert['tbsCertificate']['extensions'] as $extension) {
187
                if (
188
                    $extension['extnId'] === 'id-ce-subjectAltName'
189
                    && isset($extension['extnValue'][0]['otherName']['value']['ia5String'])
190
                ) {
191
                    $id = ltrim(
192
                        trim($extension['extnValue'][0]['otherName']['value']['ia5String']),
193
                        '0'
194
                    );
195
                    return $force_upper ? strtoupper($id) : $id;
196
                }
197
            }
198
        }
199
200
        // No se encontró el ID, se lanza excepción.
201
        throw new CertificateException(
202
            'No fue posible obtener el ID (RUN) del certificado digital (firma electrónica). Se recomienda verificar el formato y contraseña del certificado.'
203
        );
204
    }
205
206
    /**
207
     * Entrega el CN del subject.
208
     *
209
     * @return string CN del subject.
210
     */
211 1
    public function getName(): string
212
    {
213 1
        $name = $this->getData()['subject']['CN'] ?? null;
214 1
        if ($name === null) {
215
            throw new CertificateException(
216
                'No fue posible obtener el Name (subject.CN) de la firma.'
217
            );
218
        }
219
220 1
        return $name;
221
    }
222
223
    /**
224
     * Entrega el emailAddress del subject.
225
     *
226
     * @return string EmailAddress del subject.
227
     */
228 1
    public function getEmail(): string
229
    {
230 1
        $email = $this->getData()['subject']['emailAddress'] ?? null;
231 1
        if ($email === null) {
232
            throw new CertificateException(
233
                'No fue posible obtener el Email (subject.emailAddress) de la firma.'
234
            );
235
        }
236
237 1
        return $email;
238
    }
239
240
    /**
241
     * Entrega desde cuando es válida la firma.
242
     *
243
     * @return string Fecha y hora desde cuando es válida la firma.
244
     */
245 3
    public function getFrom(): string
246
    {
247 3
        return date('Y-m-d\TH:i:s', $this->getData()['validFrom_time_t']);
248
    }
249
250
    /**
251
     * Entrega hasta cuando es válida la firma.
252
     *
253
     * @return string Fecha y hora hasta cuando es válida la firma.
254
     */
255 4
    public function getTo(): string
256
    {
257 4
        return date('Y-m-d\TH:i:s', $this->getData()['validTo_time_t']);
258
    }
259
260
    /**
261
     * Entrega los días totales que la firma es válida.
262
     *
263
     * @return int Días totales en que la firma es válida.
264
     */
265
    public function getTotalDays(): int
266
    {
267
        $start = new DateTime($this->getFrom());
268
        $end = new DateTime($this->getTo());
269
        $diff = $start->diff($end);
270
        return (int) $diff->format('%a');
271
    }
272
273
    /**
274
     * Entrega los días que faltan para que la firma expire.
275
     *
276
     * @param string|null $desde Fecha desde la que se calcula.
277
     * @return int Días que faltan para que la firma expire.
278
     */
279 1
    public function getExpirationDays(?string $desde = null): int
280
    {
281 1
        if ($desde === null) {
282 1
            $desde = date('Y-m-d\TH:i:s');
283
        }
284 1
        $start = new DateTime($desde);
285 1
        $end = new DateTime($this->getTo());
286 1
        $diff = $start->diff($end);
287 1
        return (int) $diff->format('%a');
288
    }
289
290
    /**
291
     * Indica si la firma está vigente o vencida.
292
     *
293
     * NOTE: Este método también validará que la firma no esté vigente en el
294
     * futuro. O sea, que la fecha desde cuándo está vigente debe estar en el
295
     * pasado.
296
     *
297
     * @param string|null $when Fecha de referencia para validar la vigencia.
298
     * @return bool `true` si la firma está vigente, `false` si está vencida.
299
     */
300 3
    public function isActive(?string $when = null): bool
301
    {
302 3
        if ($when === null) {
303 2
            $when = date('Y-m-d');
304
        }
305
306 3
        if (!isset($when[10])) {
307 3
            $when .= 'T23:59:59';
308
        }
309
310 3
        return $when >= $this->getFrom() && $when <= $this->getTo();
311
    }
312
313
    /**
314
     * Entrega el nombre del emisor de la firma.
315
     *
316
     * @return string CN del issuer.
317
     */
318 1
    public function getIssuer(): string
319
    {
320 1
        return $this->getData()['issuer']['CN'];
321
    }
322
323
    /**
324
     * Obtiene el módulo de la clave privada.
325
     *
326
     * @return string Módulo en base64.
327
     */
328 4
    public function getModulus(int $wordwrap = Str::WORDWRAP): string
329
    {
330 4
        $modulus = $this->getPrivateKeyDetails()['rsa']['n'] ?? null;
331
332 4
        if ($modulus === null) {
333
            throw new CertificateException(
334
                'No fue posible obtener el módulo de la clave privada.'
335
            );
336
        }
337
338 4
        return Str::wordWrap(base64_encode($modulus), $wordwrap);
339
    }
340
341
    /**
342
     * Obtiene el exponente público de la clave privada.
343
     *
344
     * @return string Exponente público en base64.
345
     */
346 4
    public function getExponent(int $wordwrap = Str::WORDWRAP): string
347
    {
348 4
        $exponent = $this->getPrivateKeyDetails()['rsa']['e'] ?? null;
349
350 4
        if ($exponent === null) {
351
            throw new CertificateException(
352
                'No fue posible obtener el exponente de la clave privada.'
353
            );
354
        }
355
356 4
        return Str::wordWrap(base64_encode($exponent), $wordwrap);
357
    }
358
}
359