Issues (3)

CryptoProCli.php (3 issues)

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