| |
SSH-сессия протекает по следующим этапам:
- До соединения с сервером, пожеланию, можно установить один из двух видов публичного ключа аутентификации с сервером, например DSA или RSA. Выбрать криптографические алгоритмы, которым доверяете, и алгоритмы сжатия, если таковые имеются. Ну и конечно же, указать имя узла, на котором запущен сервер SSH.
- Соединение установлено. Безопасное рукопожатие сделано, в результате чего публичный ключ аутентификации c сервера был получен. Затем, нужно проверить легитимность данного публичного ключа, используя для примера сигнатуру MD5 или файл известных узлов.
- Клиент должен пройти процедуру аутентификации с использованием: классического способа — паролем или публичных ключей ( сгенерированных openssh от пары ключей DSA или RSA). Это можно использовать в случае, если SSH-агент запущен.
- Теперь, когда пользователь был аутентифицирован, нужно открыть один или несколько каналов. Каналы являются разновидностями туннелей по типу информации внутри одного единственного SSH-соединения. Каждый канал имеет стандартный поток (stdout) и поток ошибок (stderr). Теоретически можно открыть бесчисленное количество каналов.
- После открытия канала можно сделать несколько вещей:
- Выполнить единственную команду.
- Вызвать подсистему sftp для передачи файлов.
- Вызвать подсистему scp для передачи файлов.
- Вызвать вашу собственную подсистему. Данная операция не описывается данным документом, но читатель может сделать её самостоятельно ;-).
- По завершении всего выше перечисленного, обязательно закройте каналы и затем SSH-соединение [прим. перевод. после закрытия каналов операция закрытия SSH-соединения обязательна].
Подсистемы sftp и scp используют каналы, но libssh скрывает их от программиста. Если появится желание использовать эти подсистемы вместо каналов, обычно открывают "sftp сессию" или "scp сессию".
Создание сессии и установка опций
Наиболее важным объектом в SSH-соединении является SSH-сессия. Для того, чтобы выделить память под новую сессию используйте ssh_new(). Не забывайте всегда проверять, что выделение памяти прошло успешно.
#include <libssh/libssh.h>
#include <stdlib.h>
int main()
{
ssh_session my_ssh_session = ssh_new();
if (my_ssh_session == NULL)
exit(-1);
...
ssh_free(my_ssh_session);
}
Библиотека libssh следует правилу выделяй-освобождай (allocate-it-deallocate-it). Каждый объект, память под который была выделена с использованием xxxxx_new(), должен освобождать её используя xxxxx_free(). В этом случае, ssh_new() делает выделение памяти, а ssh_free() делает с ней обратную операцию.
Функция ssh_options_set() устанавливает опции SSH-сессии. Наиболее важные из них:
- SSH_OPTIONS_HOST: имя узла с котором будет проводится соединение.
- SSH_OPTIONS_PORT: используемый порт (по умолчанию 22);
- SSH_OPTIONS_USER: системное имя под которым будет производится соединение;
- SSH_OPTIONS_LOG_VERBOSITY: квантование печати сообщений.
Полный список опций может быть найден в документации на ssh_options_set(). Обязательной из них является опция SSH_OPTIONS_HOST. В тоже время, если опция SSH_OPTIONS_USER не используется, в качестве локального имени пользователя будет использован тот аккаунт из под которого было запущено приложение.
Ниже приводится небольшой пример, показывающий как использовать это.
#include <libssh/libssh.h>
#include <stdlib.h>
int main()
{
ssh_session my_ssh_session;
int verbosity = SSH_LOG_PROTOCOL;
int port = 22;
my_ssh_session = ssh_new();
if (my_ssh_session == NULL)
exit(-1);
ssh_options_set(my_ssh_session, SSH_OPTIONS_HOST, "localhost");
ssh_options_set(my_ssh_session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
ssh_options_set(my_ssh_session, SSH_OPTIONS_PORT, &port);
...
ssh_free(my_ssh_session);
}
Пожалуйста помните, что все параметры ssh_options_set() передаются по указателю, даже если есть необходимость установить опции целое значение.
- Смотри так же
- ssh_new
- ssh_free
- ssh_options_set
- ssh_options_parse_config
- ssh_options_copy
- ssh_options_getopt
Соединение с сервером
Раз уже все настройки сделаны, можно соединится используя ssh_connect(). Эта функция возвратит SSH_OK, если соединение работоспособно, либо SSH_ERROR в обратном случае.
Для того, чтобы показать пользователю что что-то пошло не так, можно получить сообщение об ошибке на Английском языке используя ssh_get_error(). В случае, когда необходимо завершить сессию используйте ssh_disconnect().
Ниже представлен пример
#include <libssh/libssh.h>
#include <stdlib.h>
int main()
{
ssh_session my_ssh_session;
int rc;
my_ssh_session = ssh_new();
if (my_ssh_session == NULL)
exit(-1);
ssh_options_set(my_ssh_session, SSH_OPTIONS_HOST, "localhost");
rc = ssh_connect(my_ssh_session);
if (rc != SSH_OK)
{
fprintf(stderr, "Error connecting to localhost: %s\n",
ssh_get_error(my_ssh_session));
exit(-1);
}
...
ssh_disconnect(my_ssh_session);
ssh_free(my_ssh_session);
}
Проверка подлинности сервера
После установки соединения, следующим обязательным шагом является проверка того, что сервер, с которым оно было установлено, является для использования известным и безопасным ( помните, что суть SSH в безопасности и проверки прав доступа).
Существует два способа достичь этого:
- Первый (рекомендованный) способ использовать функцию ssh_is_server_known(). Эта функция будет искать внутри файла известных узлов (~/.ssh/known_hosts на UNIX), по шаблону имя узла сервера и определять представлен ли он в списке или нет.
- Второй способ использовать ssh_get_pubkey_hash() для того, чтобы получить бинарную версию hash-значения публичного ключа. Так же можно использовать свою собственную базу данных для проверки, если этот публичный ключ вам известен и является безопасным.
Так же можно использовать ssh_get_pubkey_hash() для просмотра хэш-значения публичного ключа пользователем, в случае если он ему известен ( иногда, на всякий случай некоторые параноидальные люди записывают свои хэш-значения публичных ключей на бумагу перед тем как выйти куда-нибудь ...).
Если удаленный узел используется впервые, можно спросить пользователя (Он или Она) доверяют ли ему. Раз Он или Она заключили, что узел является действительным и стоит его добавить в файл известных узлов, используйте ssh_write_knownhost() в этом файле или другим способом, если используется собственная база данных.
Следующий пример является одним из примеров приведенных в директории examples/
#include <errno.h>
#include <string.h>
int verify_knownhost(ssh_session session)
{
int state, hlen;
unsigned char *hash = NULL;
char *hexa;
char buf[10];
state = ssh_is_server_known(session);
hlen = ssh_get_pubkey_hash(session, &hash);
if (hlen < 0)
return -1;
switch (state)
{
case SSH_SERVER_KNOWN_OK:
break; /* ok */
case SSH_SERVER_KNOWN_CHANGED:
fprintf(stderr, "Host key for server changed: it is now:\n");
ssh_print_hexa("Public key hash", hash, hlen);
fprintf(stderr, "For security reasons, connection will be stopped\n");
free(hash);
return -1;
case SSH_SERVER_FOUND_OTHER:
fprintf(stderr, "The host key for this server was not found but an other"
"type of key exists.\n");
fprintf(stderr, "An attacker might change the default server key to"
"confuse your client into thinking the key does not exist\n");
free(hash);
return -1;
case SSH_SERVER_FILE_NOT_FOUND:
fprintf(stderr, "Could not find known host file.\n");
fprintf(stderr, "If you accept the host key here, the file will be"
"automatically created.\n");
/* Возвращение к ситуации SSH_SERVER_NOT_KNOWN(прим. перевод. т.е. сервер неизвестен) */
case SSH_SERVER_NOT_KNOWN:
hexa = ssh_get_hexa(hash, hlen);
fprintf(stderr,"The server is unknown. Do you trust the host key?\n");
fprintf(stderr, "Public key hash: %s\n", hexa);
free(hexa);
if (fgets(buf, sizeof(buf), stdin) == NULL)
{
free(hash);
return -1;
}
if (strncasecmp(buf, "yes", 3) != 0)
{
free(hash);
return -1;
}
if (ssh_write_knownhost(session) < 0)
{
fprintf(stderr, "Error %s\n", strerror(errno));
free(hash);
return -1;
}
break;
case SSH_SERVER_ERROR:
fprintf(stderr, "Error %s", ssh_get_error(session));
free(hash);
return -1;
}
free(hash);
return 0;
}
- Смотри так же
- ssh_connect
- ssh_disconnect
- ssh_get_error
- ssh_get_error_code
- ssh_get_pubkey_hash
- ssh_is_server_known
- ssh_write_knownhost
Проверка подлинности пользователя
Процесс проверки подлинности пользователя [прим.перевод. он же аутентификации] является способом, когда сервис-провайдер может идентифицировать пользователя и подтвердить Его (Её) идентичность. Процесс авторизации заключается в разрешение аутентичности пользователя для доступа к ресурсам. В SSH соединены две концепции. После аутентификации, сервер может разрешить доступ пользователя к нескольким ресурсам таких как переадресация портов (port forwarding), командной строке, подсистемы sftp и так далее.
Библиотека libssh поддерживает несколько методов аутентификации:
- Метод "none". Этот метод позволяет получить доступные аутентификационные методы. Он так же дает серверу возможность аутентифицировать пользователя с Его(Её) учетной записи. Иногда очень устаревшее аппаратное обеспечение использует возможность резервирования учетной записи пользователя в стиле "telnet over SSH"
- Метод "Клавиатурно-интерактивный". Пароль отправляется на сервер, который принимается им или нет.
- Метод "Пароля". Пароль отправляется на сервер, который принимается им или нет.
- Метод "Клавиатурно-интерактивный". Сервер отправляет несколько запросов пользователю на которые тот должен корректно ответить. Такой метод делает возможным производить аутентификацию с использованием шифровальной книги, к примеру ("передать код 23:R на странице 3 ").
- Метод "Публичного ключа". Узлу известен публичный ключ пользователя и он, пользователь, должен удостоверить, что он знает связанный с ним приватный ключ. Это может быть сделано вручную или передано агенту SSH как будет показано ниже.
Все эти методы могут комбинироваться. Для примера, можно заставить пользователя производить аутентификацию по-крайне мере по двум таким методам. В таком случае, говорят о "частном случае аутентификации". Частный случай аутентификации является откликом от функции аутентификации подтверждая то, что ваша подлинность удостоверена, а другим её ещё нужно получить.
Ниже показан пример аутентификации пользователя по паролю:
#include <libssh/libssh.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
ssh_session my_ssh_session;
int rc;
char *password;
// Открытие сессии и установка опций
my_ssh_session = ssh_new();
if (my_ssh_session == NULL)
exit(-1);
ssh_options_set(my_ssh_session, SSH_OPTIONS_HOST, "localhost");
// Соединение с сервером
rc = ssh_connect(my_ssh_session);
if (rc != SSH_OK)
{
fprintf(stderr, "Error connecting to localhost: %s\n",
ssh_get_error(my_ssh_session));
ssh_free(my_ssh_session);
exit(-1);
}
// Проверка идентичности сервера
// Для получения исходного кода verify_knowhost(), обратитесь к предыдущему примеру
if (verify_knownhost(my_ssh_session) < 0)
{
ssh_disconnect(my_ssh_session);
ssh_free(my_ssh_session);
exit(-1);
}
// Аутентифицируемся сами
password = getpass("Password: ");
rc = ssh_userauth_password(my_ssh_session, NULL, password);
if (rc != SSH_AUTH_SUCCESS)
{
fprintf(stderr, "Error authenticating with password: %s\n",
ssh_get_error(my_ssh_session));
ssh_disconnect(my_ssh_session);
ssh_free(my_ssh_session);
exit(-1);
}
...
ssh_disconnect(my_ssh_session);
ssh_free(my_ssh_session);
}
- Смотри так же
- Более глубокий взгляд на аутентификацию
Сделаем что-нибудь
В этом пункте будет устанавливаться аутентификация как для клиента, так и для сервера. Пришло время воспользоваться преимуществами, которые предлагаются протоколом SSH, а именно удаленно: выполнить команду, открыть командную строку. А так же осуществить передачу файлов, переадресацию портов и многое другое.
В ниже представленном примере показано как выполнять удаленно команду:
int show_remote_processes(ssh_session session)
{
ssh_channel channel;
int rc;
char buffer[256];
unsigned int nbytes;
channel = ssh_channel_new(session);
if (channel == NULL)
return SSH_ERROR;
rc = ssh_channel_open_session(channel);
if (rc != SSH_OK)
{
ssh_channel_free(channel);
return rc;
}
rc = ssh_channel_request_exec(channel, "ps aux");
if (rc != SSH_OK)
{
ssh_channel_close(channel);
ssh_channel_free(channel);
return rc;
}
nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
while (nbytes > 0)
{
if (write(1, buffer, nbytes) != nbytes)
{
ssh_channel_close(channel);
ssh_channel_free(channel);
return SSH_ERROR;
}
nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
}
if (nbytes < 0)
{
ssh_channel_close(channel);
ssh_channel_free(channel);
return SSH_ERROR;
}
ssh_channel_send_eof(channel);
ssh_channel_close(channel);
ssh_channel_free(channel);
return SSH_OK;
}
- Смотри так же
Обработка ошибок
Все функции libssh, которые возвращают значение ошибки так же устанавливают сообщение о ней с описание проблемы на Английском языке.
Значение ошибки типично SSH_ERROR для целых значений или NULL для указателей.
Функция ssh_get_error() возвращает указатель на статическое сообщение об ошибке.
Функция ssh_error_code() возвращает код ошибки: SSH_NO_ERROR, SSH_REQUEST_DENIED, SSH_INVALID_REQUEST, SSH_CONNECTION_LOST, SSH_FATAL или SSH_INVALID_DATA. SSH_REQUEST_DENIED подразумевает, что SSH-сервер отклонил клиентский запрос, но ситуация поправима. Остальные подразумевают, что что-то случилось с соединением (какие-то криптографические проблемы, проблемы сервера, …). SSH_INVALID_REQUEST подразумевает, что библиотека получила какую-то ерунду от сервера, но может быть восстановлена. SSH_FATAL подразумевает, что соединение имеет существенную проблему не предполагающее восстановление.
Почти всегда в качестве ошибке возвращается SSH_FATAL, но иногда функции ( главным образом такие как ssh_request_xxx) могут терпеть неудачу, потому что сервер отвергает запрос. В таком случае будет возвращен SSH_REQUEST_DENIED
Функции ssh_get_error() и ssh_get_error_code() берут в качестве параметра ssh_session. Что является для потока безопасным, потому что ошибки могут быть присоединены к сессии не будучи статическими. Любая ошибка, которая произошла во время вызова ssh_options_xxx() или ssh_connect() (например, вне сессии) может быть возвращены путем NULL-указателя в качестве аргумента.
Подсистема SFTP имеет свои собственные коды ошибок в дополнение к имеющимся в libssh.
|