Passed
Push — master ( 90d55d...0e16d5 )
by Carlos C
03:42 queued 10s
created

Certificado::__construct()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 37
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6.0553

Importance

Changes 0
Metric Value
cc 6
eloc 26
nc 5
nop 2
dl 0
loc 37
ccs 23
cts 26
cp 0.8846
crap 6.0553
rs 8.8817
c 0
b 0
f 0
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
42
     * @param OpenSSL $openSSL
43
     * @throws \UnexpectedValueException when the file does not exists or is not readable
44
     * @throws \UnexpectedValueException when cannot read the certificate file or is empty
45
     * @throws \RuntimeException when cannot parse the certificate file or is empty
46
     * @throws \RuntimeException when cannot get serialNumberHex or serialNumber from certificate
47
     */
48 49
    public function __construct(string $filename, OpenSSL $openSSL = null)
49
    {
50 49
        $this->assertFileExists($filename);
51 47
        $contents = strval(file_get_contents($filename));
52 47
        if ('' === $contents) {
53 1
            throw new \UnexpectedValueException("File $filename is empty");
54
        }
55 46
        $this->setOpenSSL($openSSL ?: new OpenSSL());
56 46
        $contents = $this->obtainPemCertificate($contents);
57
58
        // get the certificate data
59 46
        $data = openssl_x509_parse($contents, true);
60 46
        if (! is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
61 1
            throw new \RuntimeException("Cannot parse the certificate file $filename");
62
        }
63
64
        // get the public key
65 45
        $pubKey = $this->obtainPubKeyFromContents($contents);
66
67
        // set all the values
68 45
        $this->certificateName = strval($data['name'] ?? '');
69 45
        $this->rfc = (string) strstr(($data['subject']['x500UniqueIdentifier'] ?? '') . ' ', ' ', true);
70 45
        $this->name = strval($data['subject']['name'] ?? '');
71 45
        $serial = new SerialNumber('');
72 45
        if (isset($data['serialNumberHex'])) {
73 45
            $serial->loadHexadecimal($data['serialNumberHex']);
74
        } elseif (isset($data['serialNumber'])) {
75
            $serial->loadDecimal($data['serialNumber']);
76
        } else {
77
            throw new \RuntimeException("Cannot get serialNumberHex or serialNumber from certificate file $filename");
78
        }
79 45
        $this->serial = $serial;
80 45
        $this->validFrom = $data['validFrom_time_t'] ?? 0;
81 45
        $this->validTo = $data['validTo_time_t'] ?? 0;
82 45
        $this->pubkey = $pubKey;
83 45
        $this->pemContents = $contents;
84 45
        $this->filename = $filename;
85 45
    }
86
87 46
    private function obtainPemCertificate(string $contents): string
88
    {
89 46
        $openssl = $this->getOpenSSL();
90 46
        $extracted = $openssl->readPemContents($contents)->certificate();
91 46
        if ('' === $extracted) { // cannot extract, could be on DER format
92 34
            $extracted = $openssl->derCerConvertPhp($contents);
93
        }
94 46
        return $extracted;
95
    }
96
97
    /**
98
     * Check if this certificate belongs to a private key
99
     *
100
     * @param string $pemKeyFile
101
     * @param string $passPhrase
102
     *
103
     * @return bool
104
     *
105
     * @throws \UnexpectedValueException if the file does not exists or is not readable
106
     * @throws \UnexpectedValueException if the file is not a PEM private key
107
     * @throws \RuntimeException if cannot open the private key file
108
     */
109 5
    public function belongsTo(string $pemKeyFile, string $passPhrase = ''): bool
110
    {
111 5
        $this->assertFileExists($pemKeyFile);
112 5
        $openSSL = $this->getOpenSSL();
113 5
        $keyContents = $openSSL->readPemContents(
114
            // intentionally silence this error, if return false then cast it to string
115 5
            strval(@file_get_contents($pemKeyFile))
116 5
        )->privateKey();
117 5
        if ('' === $keyContents) {
118 1
            throw new \UnexpectedValueException("The file $pemKeyFile is not a PEM private key");
119
        }
120 4
        $privateKey = openssl_get_privatekey($keyContents, $passPhrase);
121 4
        if (false === $privateKey) {
122 1
            throw new \RuntimeException("Cannot open the private key file $pemKeyFile");
123
        }
124 3
        $belongs = openssl_x509_check_private_key($this->getPemContents(), $privateKey);
125 3
        openssl_free_key($privateKey);
126 3
        return $belongs;
127
    }
128
129
    /**
130
     * RFC (Registro Federal de Contribuyentes) set when certificate was created
131
     * @return string
132
     */
133 30
    public function getRfc(): string
134
    {
135 30
        return $this->rfc;
136
    }
137
138 1
    public function getCertificateName(): string
139
    {
140 1
        return $this->certificateName;
141
    }
142
143
    /**
144
     * Name (Razón Social) set when certificate was created
145
     * @return string
146
     */
147 28
    public function getName(): string
148
    {
149 28
        return $this->name;
150
    }
151
152
    /**
153
     * Certificate serial number as ASCII, this data is in the format required by CFDI
154
     * @return string
155
     */
156 31
    public function getSerial(): string
157
    {
158 31
        return $this->serial->asAscii();
159
    }
160
161 2
    public function getSerialObject(): SerialNumber
162
    {
163 2
        return clone $this->serial;
164
    }
165
166
    /**
167
     * Timestamp since the certificate is valid
168
     * @return int
169
     */
170 19
    public function getValidFrom(): int
171
    {
172 19
        return $this->validFrom;
173
    }
174
175
    /**
176
     * Timestamp until the certificate is valid
177
     * @return int
178
     */
179 19
    public function getValidTo(): int
180
    {
181 19
        return $this->validTo;
182
    }
183
184
    /**
185
     * String representation of the public key
186
     * @return string
187
     */
188 19
    public function getPubkey(): string
189
    {
190 19
        return $this->pubkey;
191
    }
192
193
    /**
194
     * Place where the certificate was when loaded, it might not exists on the file system
195
     * @return string
196
     */
197 18
    public function getFilename(): string
198
    {
199 18
        return $this->filename;
200
    }
201
202
    /**
203
     * The contents of the certificate in PEM format
204
     * @return string
205
     */
206 7
    public function getPemContents(): string
207
    {
208 7
        return $this->pemContents;
209
    }
210
211
    /**
212
     * Verify the signature of some data
213
     *
214
     * @param string $data
215
     * @param string $signature
216
     * @param int $algorithm
217
     *
218
     * @return bool
219
     *
220
     * @throws \RuntimeException if cannot open the public key from certificate
221
     * @throws \RuntimeException if openssl report an error
222
     */
223 18
    public function verify(string $data, string $signature, int $algorithm = OPENSSL_ALGO_SHA256): bool
224
    {
225 18
        $pubKey = openssl_get_publickey($this->getPubkey());
226 18
        if (false === $pubKey) {
227
            throw new \RuntimeException('Cannot open public key from certificate');
228
        }
229
        try {
230 18
            $verify = openssl_verify($data, $signature, $pubKey, $algorithm);
231 18
            if (-1 === $verify) {
232
                throw new \RuntimeException('OpenSSL Error: ' . openssl_error_string());
233
            }
234 18
        } finally {
235 18
            openssl_free_key($pubKey);
236
        }
237 18
        return (1 === $verify);
238
    }
239
240
    /**
241
     * @param string $filename
242
     * @throws \UnexpectedValueException when the file does not exists or is not readable
243
     * @return void
244
     */
245 49
    protected function assertFileExists(string $filename)
246
    {
247 49
        if (! file_exists($filename) || ! is_readable($filename) || is_dir($filename)) {
248 2
            throw new \UnexpectedValueException("File $filename does not exists or is not readable");
249
        }
250 47
    }
251
252 45
    protected function obtainPubKeyFromContents(string $contents): string
253
    {
254
        try {
255 45
            $pubkey = openssl_get_publickey($contents);
256 45
            if (! is_resource($pubkey)) {
257
                return '';
258
            }
259 45
            $pubData = openssl_pkey_get_details($pubkey) ?: [];
260 45
            return $pubData['key'] ?? '';
261
        } finally {
262
            // close public key even if the flow is throw an exception
263 45
            if (is_resource($pubkey)) {
264 45
                openssl_free_key($pubkey);
265
            }
266
        }
267
    }
268
}
269