Completed
Push — master ( 5c0137...35a95b )
by Carlos C
03:16 queued 10s
created

Certificado   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Test Coverage

Coverage 94.69%

Importance

Changes 13
Bugs 0 Features 0
Metric Value
eloc 111
c 13
b 0
f 0
dl 0
loc 298
ccs 107
cts 113
cp 0.9469
rs 8.96
wmc 43

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getPemContents() 0 3 1
A getPubkey() 0 3 1
A getSerialObject() 0 3 1
A belongsTo() 0 18 3
A getName() 0 3 1
A verify() 0 15 3
A getSerial() 0 3 1
A getValidFrom() 0 3 1
A getFilename() 0 3 1
A getCertificateName() 0 3 1
A obtainPemCertificate() 0 8 2
A getRfc() 0 3 1
A getValidTo() 0 3 1
B __construct() 0 48 9
B assertFileExists() 0 18 7
A obtainPubKeyFromContents() 0 13 4
A extractPemCertificate() 0 13 5

How to fix   Complexity   

Complex Class

Complex classes like Certificado often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Certificado, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace CfdiUtils\Certificado;
3
4
use CfdiUtils\OpenSSL\OpenSSL;
5
use CfdiUtils\OpenSSL\OpenSSLPropertyTrait;
6
7
class Certificado
8
{
9
    use OpenSSLPropertyTrait;
10
11
    /** @var string */
12
    private $rfc;
13
14
    /** @var string */
15
    private $certificateName;
16
17
    /** @var string */
18
    private $name;
19
20
    /** @var SerialNumber */
21
    private $serial;
22
23
    /** @var int */
24
    private $validFrom;
25
26
    /** @var int */
27
    private $validTo;
28
29
    /** @var string */
30
    private $pubkey;
31
32
    /** @var string */
33
    private $filename;
34
35
    /** @var string */
36
    private $pemContents;
37
38
    /**
39
     * Certificado constructor.
40
     *
41
     * @param string $filename Allows filename or certificate contents (PEM or DER)
42
     * @param OpenSSL $openSSL
43
     * @throws \UnexpectedValueException when the certificate does not exists or is not readable
44
     * @throws \UnexpectedValueException when cannot read the certificate or is empty
45
     * @throws \RuntimeException when cannot parse the certificate or is empty
46
     * @throws \RuntimeException when cannot get serialNumberHex or serialNumber from certificate
47
     */
48 54
    public function __construct(string $filename, OpenSSL $openSSL = null)
49
    {
50 54
        $this->setOpenSSL($openSSL ?: new OpenSSL());
51 54
        $contents = $this->extractPemCertificate($filename);
52
        // using $filename as PEM content did not retrieve any result,
53
        // or the path actually exists (path is a valid base64 string)
54
        // then use it as path
55 54
        if ('' === $contents || realpath($filename)) {
56 41
            $sourceName = 'file ' . $filename;
57 41
            $this->assertFileExists($filename);
58 38
            $contents = file_get_contents($filename) ?: '';
59 38
            if ('' === $contents) {
60 1
                throw new \UnexpectedValueException("File $filename is empty");
61
            }
62
            // this will take PEM contents or perform a PHP conversion from DER to PEM
63 37
            $contents = $this->obtainPemCertificate($contents);
64
        } else {
65 30
            $filename = '';
66 30
            $sourceName = '(contents)';
67
        }
68
69
        // get the certificate data
70 50
        $data = openssl_x509_parse($contents, true);
71 50
        if (! is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
72 1
            throw new \RuntimeException("Cannot parse the certificate $sourceName");
73
        }
74
75
        // get the public key
76 49
        $pubKey = $this->obtainPubKeyFromContents($contents);
77
78
        // set all the values
79 49
        $this->certificateName = strval($data['name'] ?? '');
80 49
        $this->rfc = (string) strstr(($data['subject']['x500UniqueIdentifier'] ?? '') . ' ', ' ', true);
81 49
        $this->name = strval($data['subject']['name'] ?? '');
82 49
        $serial = new SerialNumber('');
83 49
        if (isset($data['serialNumberHex'])) {
84 49
            $serial->loadHexadecimal($data['serialNumberHex']);
85
        } elseif (isset($data['serialNumber'])) {
86
            $serial->loadDecimal($data['serialNumber']);
87
        } else {
88
            throw new \RuntimeException("Cannot get serialNumberHex or serialNumber from certificate $sourceName");
89
        }
90 49
        $this->serial = $serial;
91 49
        $this->validFrom = $data['validFrom_time_t'] ?? 0;
92 49
        $this->validTo = $data['validTo_time_t'] ?? 0;
93 49
        $this->pubkey = $pubKey;
94 49
        $this->pemContents = $contents;
95 49
        $this->filename = $filename;
96 49
    }
97
98 54
    private function extractPemCertificate(string $contents): string
99
    {
100 54
        $openssl = $this->getOpenSSL();
101 54
        $decoded = @base64_decode($contents, true) ?: '';
102 54
        if ('' !== $decoded && $contents === base64_encode($decoded)) { // is a one liner certificate
103 30
            $doubleEncoded = $openssl->readPemContents($decoded)->certificate();
104 30
            if ($doubleEncoded !== '') {
105 9
                return $doubleEncoded;
106
            }
107
            // derCerConvertPhp will include PEM header and footer
108 21
            $contents = $this->getOpenSSL()->derCerConvertPhp($decoded);
109
        }
110 45
        return $openssl->readPemContents($contents)->certificate();
111
    }
112
113 37
    private function obtainPemCertificate(string $contents): string
114
    {
115 37
        $openssl = $this->getOpenSSL();
116 37
        $extracted = $openssl->readPemContents($contents)->certificate();
117 37
        if ('' === $extracted) { // cannot extract, could be on DER format
118 33
            $extracted = $this->getOpenSSL()->derCerConvertPhp($contents);
119
        }
120 37
        return $extracted;
121
    }
122
123
    /**
124
     * Check if this certificate belongs to a private key
125
     *
126
     * @param string $pemKeyFile
127
     * @param string $passPhrase
128
     *
129
     * @return bool
130
     *
131
     * @throws \UnexpectedValueException if the file does not exists or is not readable
132
     * @throws \UnexpectedValueException if the file is not a PEM private key
133
     * @throws \RuntimeException if cannot open the private key file
134
     */
135 5
    public function belongsTo(string $pemKeyFile, string $passPhrase = ''): bool
136
    {
137 5
        $this->assertFileExists($pemKeyFile);
138 5
        $openSSL = $this->getOpenSSL();
139 5
        $keyContents = $openSSL->readPemContents(
140
            // intentionally silence this error, if return false then cast it to string
141 5
            strval(@file_get_contents($pemKeyFile))
142 5
        )->privateKey();
143 5
        if ('' === $keyContents) {
144 1
            throw new \UnexpectedValueException("The file $pemKeyFile is not a PEM private key");
145
        }
146 4
        $privateKey = openssl_get_privatekey($keyContents, $passPhrase);
147 4
        if (false === $privateKey) {
148 1
            throw new \RuntimeException("Cannot open the private key file $pemKeyFile");
149
        }
150 3
        $belongs = openssl_x509_check_private_key($this->getPemContents(), $privateKey);
151 3
        openssl_free_key($privateKey);
152 3
        return $belongs;
153
    }
154
155
    /**
156
     * RFC (Registro Federal de Contribuyentes) set when certificate was created
157
     * @return string
158
     */
159 32
    public function getRfc(): string
160
    {
161 32
        return $this->rfc;
162
    }
163
164 1
    public function getCertificateName(): string
165
    {
166 1
        return $this->certificateName;
167
    }
168
169
    /**
170
     * Name (Razón Social) set when certificate was created
171
     * @return string
172
     */
173 30
    public function getName(): string
174
    {
175 30
        return $this->name;
176
    }
177
178
    /**
179
     * Certificate serial number as ASCII, this data is in the format required by CFDI
180
     * @return string
181
     */
182 34
    public function getSerial(): string
183
    {
184 34
        return $this->serial->asAscii();
185
    }
186
187 2
    public function getSerialObject(): SerialNumber
188
    {
189 2
        return clone $this->serial;
190
    }
191
192
    /**
193
     * Timestamp since the certificate is valid
194
     * @return int
195
     */
196 21
    public function getValidFrom(): int
197
    {
198 21
        return $this->validFrom;
199
    }
200
201
    /**
202
     * Timestamp until the certificate is valid
203
     * @return int
204
     */
205 21
    public function getValidTo(): int
206
    {
207 21
        return $this->validTo;
208
    }
209
210
    /**
211
     * String representation of the public key
212
     * @return string
213
     */
214 21
    public function getPubkey(): string
215
    {
216 21
        return $this->pubkey;
217
    }
218
219
    /**
220
     * Place where the certificate was when loaded, it might not exists on the file system
221
     * @return string
222
     */
223 2
    public function getFilename(): string
224
    {
225 2
        return $this->filename;
226
    }
227
228
    /**
229
     * The contents of the certificate in PEM format
230
     * @return string
231
     */
232 23
    public function getPemContents(): string
233
    {
234 23
        return $this->pemContents;
235
    }
236
237
    /**
238
     * Verify the signature of some data
239
     *
240
     * @param string $data
241
     * @param string $signature
242
     * @param int $algorithm
243
     *
244
     * @return bool
245
     *
246
     * @throws \RuntimeException if cannot open the public key from certificate
247
     * @throws \RuntimeException if openssl report an error
248
     */
249 20
    public function verify(string $data, string $signature, int $algorithm = OPENSSL_ALGO_SHA256): bool
250
    {
251 20
        $pubKey = openssl_get_publickey($this->getPubkey());
252 20
        if (false === $pubKey) {
253
            throw new \RuntimeException('Cannot open public key from certificate');
254
        }
255
        try {
256 20
            $verify = openssl_verify($data, $signature, $pubKey, $algorithm);
257 20
            if (-1 === $verify) {
258
                throw new \RuntimeException('OpenSSL Error: ' . openssl_error_string());
259
            }
260 20
        } finally {
261 20
            openssl_free_key($pubKey);
262
        }
263 20
        return (1 === $verify);
264
    }
265
266
    /**
267
     * @param string $filename
268
     * @throws \UnexpectedValueException when the file does not exists or is not readable
269
     * @return void
270
     */
271 41
    protected function assertFileExists(string $filename)
272
    {
273 41
        $exists = false;
274 41
        $previous = null;
275
        try {
276 41
            if (boolval(preg_match('/[[:cntrl:]]/', $filename))) {
277 1
                $filename = '(invalid file name)';
278 1
                throw new \RuntimeException('The file name contains control characters, it might be a DER content');
279
            }
280 40
            if (file_exists($filename) && is_readable($filename) && ! is_dir($filename)) {
281 40
                $exists = true;
282
            }
283 1
        } catch (\Throwable $exception) {
284 1
            $previous = $exception;
285
        }
286 41
        if (! $exists) {
287 3
            $exceptionMessage = sprintf('File %s does not exists or is not readable', $filename);
288 3
            throw new \UnexpectedValueException($exceptionMessage, 0, $previous);
289
        }
290 38
    }
291
292 49
    protected function obtainPubKeyFromContents(string $contents): string
293
    {
294
        try {
295 49
            $pubkey = openssl_get_publickey($contents);
296 49
            if (! is_resource($pubkey)) {
297
                return '';
298
            }
299 49
            $pubData = openssl_pkey_get_details($pubkey) ?: [];
300 49
            return $pubData['key'] ?? '';
301
        } finally {
302
            // close public key even if the flow is throw an exception
303 49
            if (is_resource($pubkey)) {
304 49
                openssl_free_key($pubkey);
305
            }
306
        }
307
    }
308
}
309