Passed
Pull Request — master (#8)
by
unknown
01:26
created

CryptoProCli::getSigns()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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