Passed
Pull Request — master (#6)
by
unknown
07:17
created

CryptoProCli::verifyFile()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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