Passed
Push — master ( b66df7...5c9391 )
by Nikita
07:39 queued 06:15
created

CryptoProCli::proxyCurl()   A

Complexity

Conditions 6
Paths 32

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 11
nc 32
nop 6
dl 0
loc 21
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
namespace nikserg\cryptoprocli;
4
5
use nikserg\cryptoprocli\Exception\Cli;
6
use nikserg\cryptoprocli\Exception\SignatureError;
7
8
/**
9
 * Class CryptoProCli
10
 *
11
 * Функции для работы с консольной утилитой КриптоПро
12
 *
13
 *
14
 * @package nikserg\cryptoprocli
15
 */
16
class CryptoProCli
17
{
18
    /**
19
     * @var bool Небезопасный режим - когда цепочка подтверждения подписи не проверяется.
20
     * Включение даст возможность использовать самоподписанные сертификаты.
21
     */
22
    private bool $nochain;
23
24
    /**
25
     * @var string Путь к исполняемому файлу cryptcp КриптоПро
26
     */
27
    public string $cryptcpExec = '/opt/cprocsp/bin/amd64/cryptcp';
28
29
    /**
30
     * @var string Путь к исполняемому файлу certmgr КриптоПро
31
     */
32
    public string $certmgrExec = '/opt/cprocsp/bin/amd64/certmgr';
33
34
    /**
35
     * @var string Путь к исполняемому файлу curl КриптоПро
36
     */
37
    public string $curlExec = '/opt/cprocsp/bin/amd64/curl';
38
39
    public function __construct(bool $nochain = false)
40
    {
41
        $this->nochain = $nochain;
42
    }
43
44
    /**
45
     * Возвращает exec в зависимостри от ОС
46
     *
47
     *
48
     * @param string $path
49
     * @return string
50
     */
51
    private static function getExec(string $path): string
52
    {
53
        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
54
            return '"' . $path . '"';
55
        } else {
56
            return $path;
57
        }
58
    }
59
60
    /**
61
     * Получить список всех подписей
62
     *
63
     *
64
     * @return string|false|null
65
     */
66
    public function getSigns(): string|false|null
67
    {
68
        return shell_exec(self::getExec($this->certmgrExec) . ' -list -store uMy');
69
    }
70
71
    /**
72
     * Подписать ранее неподписанный файл
73
     *
74
     *
75
     * @param string $file Путь к подписываемому файлу
76
     * @param string|array $thumbprint SHA1 hash подписи, либо неассоциативный массив собержащий thumbprint и pin пароль ключевого контейнера
77
     * @param string $toFile
78
     * @param bool $detached Создать открепленную подпись
79
     * @throws Cli
80
     */
81
    public function signFile(string $file, string|array $thumbprint, string $toFile = '', bool $detached = false): void
82
    {
83
        list($hash, $pin) = is_array($thumbprint) ? $thumbprint : [$thumbprint, ''];
0 ignored issues
show
introduced by
The condition is_array($thumbprint) is always true.
Loading history...
84
        $shellCommand = self::getExec($this->cryptcpExec)
85
            . ' -sign'
86
            . ($detached ? ' -detached' : '')
87
            . ($this->nochain ? ' -nochain' : '')
88
            . ' -thumbprint ' . $hash
89
            . ($pin ? ' -pin ' . $pin : '')
90
            . ' ' . $file . ' ' . $toFile;
91
        $result = shell_exec($shellCommand);
92
93
        if (strpos($result, "Signed message is created.") <= 0 && strpos($result,
94
                "Подписанное сообщение успешно создано") <= 0) {
95
            throw new Cli('В ответе Cryptcp не найдена строка "Signed message is created" или "Подписанное сообщение успешно создано": ' . $result . ' команда ' . $shellCommand);
96
        }
97
    }
98
99
    /**
100
     * Подписать данные
101
     *
102
     *
103
     * @param string $data Строка подписываемых данных
104
     * @param string|array $thumbprint SHA1 hash подписи, либо неассоциативный массив собержащий thumbprint и pin пароль ключевого контейнера
105
     * @return bool|string
106
     * @throws Cli
107
     */
108
    public function signData(string $data, string|array $thumbprint): bool|string
109
    {
110
        $from = tempnam('/tmp', 'cpsign');
111
        $to = tempnam('/tmp', 'cpsign');
112
        file_put_contents($from, $data);
113
114
        $this->signFile($from, $thumbprint, $to);
115
        unlink($from);
116
        $return = file_get_contents($to);
117
        unlink($to);
118
119
        return $return;
120
    }
121
122
    /**
123
     * Добавить подпись в файл, уже содержащий подпись
124
     *
125
     *
126
     * @param string $file Путь к подписываемому файлу
127
     * @param string|array $thumbprint SHA1 hash подписи, либо неассоциативный массив собержащий thumbprint и pin пароль ключевого контейнера
128
     * @throws Cli
129
     */
130
    public function addSignToFile(string $file, string|array $thumbprint): void
131
    {
132
        list($hash, $pin) = is_array($thumbprint) ? $thumbprint : [$thumbprint, ''];
0 ignored issues
show
introduced by
The condition is_array($thumbprint) is always true.
Loading history...
133
        $shellCommand = self::getExec($this->cryptcpExec)
134
            . ' -addsign'
135
            . ($this->nochain ? ' -nochain' : '')
136
            . ' -thumbprint ' . $hash
137
            . ($pin ? ' -pin ' . $pin : '')
138
            . ' ' . $file;
139
        $result = shell_exec($shellCommand);
140
141
        if (strpos($result, "Signed message is created.") <= 0) {
142
            throw new Cli('В ответе Cryptcp не найдена строка Signed message is created: ' . $result . ' команда ' . $shellCommand);
143
        }
144
    }
145
146
    /**
147
     * Проверить, что содержимое файла подписано правильной подписью
148
     *
149
     *
150
     * @param string $fileContent
151
     * @throws Cli
152
     * @throws SignatureError
153
     */
154
    public function verifyFileContent(string $fileContent): void
155
    {
156
        $file = tempnam(sys_get_temp_dir(), 'cpc');
157
        file_put_contents($file, $fileContent);
158
        try {
159
            $this->verifyFile($file);
160
        } finally {
161
            unlink($file);
162
        }
163
    }
164
165
    /**
166
     * Проверить, что содержимое файла подписано правильной подписью открепленной подписью
167
     *
168
     *
169
     * @param string $fileSignContent
170
     * @param string $fileToBeSignedContent
171
     * @throws Cli
172
     * @throws SignatureError
173
     */
174
    public function verifyFileContentDetached(string $fileSignContent, string $fileToBeSignedContent): void
175
    {
176
        $fileToBeSigned = tempnam(sys_get_temp_dir(), 'detach');
177
        $fileSign = $fileToBeSigned . '.sgn';
178
        file_put_contents($fileSign, $fileSignContent);
179
        file_put_contents($fileToBeSigned, $fileToBeSignedContent);
180
        try {
181
            $this->verifyFileDetached($fileSign, $fileToBeSigned, sys_get_temp_dir());
182
        } finally {
183
            unlink($fileSign);
184
            unlink($fileToBeSigned);
185
        }
186
    }
187
188
    private static function getDevNull(): string
189
    {
190
        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
191
            return 'NUL';
192
        }
193
        return '/dev/null';
194
    }
195
196
    const ERROR_CODE_WRONG_SIGN = '0x200001f9';
197
    const ERROR_CODE_WRONG_CHAIN = '0x20000133';
198
    const ERROR_CODE_NO_CERTS = '0x2000012d';
199
    const ERROR_CODE_MULTIPLE_CERTS = '0x2000012e';
200
    const ERROR_CODE_UNTRUSTED_ROOT = '0x20000131';
201
    const ERROR_CODE_MESSAGE = [
202
        self::ERROR_CODE_WRONG_CHAIN    => 'Цепочка сертификатов не проверена',
203
        self::ERROR_CODE_WRONG_SIGN     => 'Подпись не верна',
204
        self::ERROR_CODE_NO_CERTS       => 'Сертификаты не найдены',
205
        self::ERROR_CODE_MULTIPLE_CERTS => 'Более одного сертификата',
206
        self::ERROR_CODE_UNTRUSTED_ROOT => 'Нет доверия к корневому сертификату',
207
    ];
208
209
    /**
210
     * Проверить, что файл подписан правильной подписью
211
     *
212
     *
213
     * @param string $file
214
     * @throws Cli
215
     * @throws SignatureError
216
     */
217
    public function verifyFile(string $file): void
218
    {
219
        $shellCommand = 'yes "n" 2> ' . self::getDevNull() . ' | ' . escapeshellarg($this->cryptcpExec) . ' -verify -verall ' . escapeshellarg($file);
220
        $result = shell_exec($shellCommand);
221
        if (!str_contains($result, "[ErrorCode: 0x00000000]") && !str_contains($result, "[ReturnCode: 0]")) {
222
            preg_match('#\[ErrorCode: (.+)]#', $result, $matches);
223
            $code = strtolower($matches[1]);
224
            if (isset(self::ERROR_CODE_MESSAGE[$code])) {
225
                $message = self::ERROR_CODE_MESSAGE[$code];
226
227
                //Дополнительная расшифровка ошибки
228
                if (str_contains($result, 'The certificate or certificate chain is based on an untrusted root')) {
229
                    $message .= ' - нет доверия к корневому сертификату УЦ, выпустившего эту подпись.';
230
                }
231
                throw new SignatureError($message, $code);
232
            }
233
            throw new Cli("Неожиданный результат $shellCommand: \n$result");
234
        }
235
    }
236
237
    /**
238
     * Проверить, что файл подписан правильной открепленной подписью
239
     *
240
     *
241
     * @param string $fileSign
242
     * @param string $fileToBeSigned
243
     * @param string $fileDir
244
     * @throws Cli
245
     * @throws SignatureError
246
     */
247
    public function verifyFileDetached(string $fileSign, string $fileToBeSigned, string $fileDir): void
248
    {
249
        $shellCommand = 'yes "n" 2> ' . self::getDevNull() . ' | ' . escapeshellarg($this->cryptcpExec) . ' -vsignf -dir '
250
            . escapeshellarg($fileDir) . ' '
251
            . escapeshellarg($fileToBeSigned)
252
            . ' -f ' . escapeshellarg($fileSign);
253
        $result = shell_exec($shellCommand);
254
255
        if (!str_contains($result, "[ErrorCode: 0x00000000]") && !str_contains($result, "[ReturnCode: 0]")) {
256
            preg_match('#\[ErrorCode: (.+)]#', $result, $matches);
257
            $code = strtolower($matches[1]);
258
            if (isset(self::ERROR_CODE_MESSAGE[$code])) {
259
                throw new SignatureError(self::ERROR_CODE_MESSAGE[$code], $code);
260
            }
261
            throw new Cli("Неожиданный результат $shellCommand: \n$result");
262
        }
263
    }
264
265
    /**
266
     * Curl-запросы с использованием гостовых сертификатов
267
     *
268
     *
269
     * @param string $url
270
     * @param string|array $thumbprint
271
     * @param string $method
272
     * @param string|null $bearer
273
     * @param string|null $contentType
274
     * @param string|null $data
275
     * @return bool|string
276
     */
277
    public function proxyCurl(
278
        string $url,
279
        string|array $thumbprint,
280
        string $method = 'GET',
281
        string $bearer = null,
282
        string $contentType = null,
283
        string $data = null
284
    ): bool|string
285
    {
286
        list($hash, $pin) = is_array($thumbprint) ? $thumbprint : [$thumbprint, ''];
0 ignored issues
show
introduced by
The condition is_array($thumbprint) is always true.
Loading history...
287
        $shellCommand = self::getExec($this->curlExec)
288
            . ' -k -s -X ' . $method
289
            . ' ' . $url
290
            . ' --cert-type CERT_SHA1_HASH_PROP_ID:CERT_SYSTEM_STORE_CURRENT_USER:My'
291
            . ' --cert ' . $hash
292
            . ($pin ? ' --pass ' . $pin : '')
293
            . ($bearer ? ' --header \"Authorization: Bearer ' . $bearer . '\"' : '')
294
            . ($contentType ? ' --header \"Content-Type: ' . $contentType . '\"' : '')
295
            . ($data ? ' --data \'' . str_replace("'", "'\''", $data) . '\'' : '');
296
297
        return shell_exec($shellCommand);
0 ignored issues
show
Bug Best Practice introduced by
The expression return shell_exec($shellCommand) could return the type null which is incompatible with the type-hinted return boolean|string. Consider adding an additional type-check to rule them out.
Loading history...
298
    }
299
}
300