Passed
Push — master ( c02ff8...8cf6c5 )
by Esteban De La Fuente
06:05
created

Certificate::getPrivateKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.9765

Importance

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