Перевод статьи «Asylo Quickstart Guide»
Как мы писали ранее, Google анонсировала новый фреймворк, который позволяет выносить часть функциональности приложения в защищённую среду выполнения, называемую анклав. В данной статье разберёмся, как использовать этот фреймворк.
Что такое анклав
В традиционных системах ядро ОС имеет неограниченный доступ к аппаратным ресурсам машины. Ядро обычно предоставляет большинство прав доступа root-пользователю без каких-либо ограничений. Кроме того, root-пользователь может расширять или изменять ядро в запущенной системе. В результате, если злоумышленник сможет выполнить код с root-привилегиями, он сможет скомпрометировать все секретные данные и обойти все политики безопасности на машине.
Анклавы — это новая парадигма, которая меняет положение дел. Анклав — это особый контекст выполнения, в котором код может работать будучи защищённым даже от ядра ОС. Это значит, что даже пользователь с root-правами не сможет извлечь секреты анклава или поставить под угрозу его целостность. Такая защита обеспечивается с помощью технологий аппаратной изоляции, таких как Intel SGX или ARM TrustZone, или даже с помощью дополнительных программных уровней, таких как гипервизор. Эти технологии позволяют создавать новые формы изоляции за пределами обычного разделения ядро-пользователь.
Новые функции безопасности привлекательны для разработчиков защищённых приложений, но на практике существует большая разница между наличием возможностей и разработкой приложений, которые используют эту возможность. Для создания полезных приложений в анклаве требуются инструменты для создания, загрузки и управления анклавами. Для полноценной работы в анклаве требуется поддержка языков программирования и доступ к библиотекам основных платформ.
Что такое Asylo
Asylo — это open-source фреймворк для разработки приложений в анклаве. Он определяет абстрактную модель анклава, которую можно привязать к различным анклавным технологиям (например, анклавный бэкенд). Asylo предоставляет платформу для разработки программного обеспечения, которая поддерживает всё более широкий спектр вариантов использования.
Далее мы шаг за шагом напишем простой анклав. В примере будет показана инициализация анклава, передача аргументов коду, работающему внутри анклава, и возвращение результатов. Несмотря на то, что это довольно простой пример, он демонстрирует основную функциональность Asylo и шаги, которым нужно следовать для использования этой функциональности.
Начало работы
Запустите следующие команды, чтобы загрузить наш Docker-контейнер и исходники, которые мы будем использовать Прочитайте README для дополнительных инструкций по использованию Docker.
docker pull gcr.io/asylo-framework/asylo
MY_PROJECT=~/asylo-examples
mkdir -p "${MY_PROJECT}"
wget -q -O - https://asylo.dev/asylo-examples.tar.gz | \
tar -zxv --directory "${MY_PROJECT}"
На месте MY_PROJECT
вы можете указать любую директорию. Эта переменная среды затем будет использоваться в инструкциях по созданию и запуску приложения в анклаве.
Примеры кода можно найти в репозитории Asylo SDK на GitHub.
Общий подход
В Asylo анклав работает в контексте пользовательского приложения. Тем не менее, по соображениям безопасности и компактности, Asylo не поддерживает прямое взаимодействие кода анклава и ОС. Вместо этого для всех таких взаимодействий требуется посредник в виде кода, который работает за пределами анклава. Такой код мы называем недоверенным приложением, в то время как код, запущенный в анклаве, называем доверенным приложением или просто анклавом.
В данном гайде мы фокусируемся на модели, где основная часть пользовательской логики находится внутри анклава. В этой модели разработчику возможно придётся написать определённое количество шаблонного кода (похожего на тот, что будет далее этой статье), однако большая часть кода, необходимого для создания, запуска и взаимодействия с анклавами предусмотрена фреймворком Asylo.
Asylo использует объектно-ориентированный подход к разработке приложений в анклаве. По сути анклав представляет собой набор частных данных и логики вместе с публичными методами для доступа к ним. С этой целью Asylo создаёт анклав, используя TrustedApplication
, абстрактный класс, определяющий разные точки входа в анклав. Для реализации приложения в анклаве разработчику нужно создать подкласс TrustedApplication
и реализовать соответствующие методы.
В этой статье мы будем ссылаться на экземпляр TrustedApplication
, называя его как «доверенное приложение», так и «анклав».
Модель взаимодействия анклавов
В Asylo анклавы работают с protocol-buffer сообщениями; все входы и выходы анклава представляют собой protocol buffer.
Процесс перехода с ненадёжного приложения к анклаву мы назовём входом в анклав, а обратный процесс — выходом из анклава.
В Asylo все анклавные взаимодействия обрабатываются через абстрактный класс EnclaveClient
. Asylo предоставляет конкретные реализации этого класса для всех поддерживаемых анклавных технологий. В классе EnclaveClient
определено несколько методов для входа в анклав. С другой стороны, выход из анклава проходит неявно — это происходит автоматически, когда анклав завершает работу или когда анклав обращается к службам операционной системы.
Среди множества методов входа в анклав, определённых интерфейсом EnclaveClient
, три представляют особый интерес для пользователей Asylo:
EnterAndInitialize
: Этот метод принимает сообщениеEnclaveConfig
, которое содержит основные настройки конфигурации анклава, и передаёт его в анклав. Это приватный метод и он неявно вызывается фреймворком Asylo при загрузке бинарного образа анклава.EnterAndRun
: Этот метод принимает сообщениеEnclaveInput
и передаёт его в анклав, который в результате может вернутьEnclaveOutput
. СообщенияEnclaveInput
иEnclaveOutput
можно расширить с помощью protobuf-расширений для удовлетворения требований к обработке данных в приложении. Этот метод публичный и его можно вызывать произвольное количество раз с разными входными данными после инициализации анклава.EnterAndFinalize
: Этот метод принимает сообщениеEnclaveFinal
, в котором может содержаться информация, необходимая для финализации анклава, и передаёт это сообщение анклаву прямо перед его уничтожением. Это приватный метод классаEnclaveClient
, который неявно вызывается фреймворком при уничтожении анклава.
Каждый EnclaveClient
связан ровно с одним анклавом и Asylo передаёт вызовы к методам EnclaveClient
соответствующим методам анклава в соответствующем экземпляре TrustedApplication
, который может быть переопределён пользователем анклава.
В интерфейсе TrustedApplication
объявлены методы, соответствующие трём входным методам, определённым абстрактным классом EnclaveClient
:
Initialize
: Этот метод принимает сообщениеEnclaveConfig
изEnclaveClient::EnterAndInitialize
и инициализирует анклав, используя настройки конфигурации вEnclaveConfig
.Run
: Этот метод принимает сообщениеEnclaveInput
изEnclaveClient::EnterAndRun
, возвращает сообщениеEnclaveOutput
и проводит доверенное выполнение.Finalize
: Этот метод принимает сообщениеEnclaveFinal
изEnclaveClient::EnterAndFinalize
и готовит анклав к уничтожению.
Жизненный цикл анклава
Вход в анклав аналогичен системному вызову. Точка входа в анклав представляет собой интерфейс для защищённого кода с доступом к ресурсам анклава. Аргументы копируются в защищённую память анклава при входе, а результаты копируются при выходе.
DEFINE_string(enclave_path, "", "Path to enclave binary image to load");
DEFINE_string(message, "", "Message to encrypt");
int main(int argc, char *argv[]) {
google::ParseCommandLineFlags(&argc, &argv, /*remove_flags=*/true);
LOG_IF(QFATAL, FLAGS_message.empty()) << "Empty --message flag";
// Инициализация
asylo::EnclaveManager::Configure(asylo::EnclaveManagerOptions());
auto manager_result = asylo::EnclaveManager::Instance();
LOG_IF(QFATAL, !manager_result.ok()) << "Could not obtain EnclaveManager";
asylo::EnclaveManager *manager = manager_result.ValueOrDie();
asylo::SimLoader loader(FLAGS_enclave_path, /*debug=*/true);
asylo::Status status = manager->LoadEnclave("demo_enclave", loader);
LOG_IF(QFATAL, !status.ok()) << "LoadEnclave failed with: " << status;
// Безопасное выполнение
asylo::EnclaveClient *client = manager->GetClient("demo_enclave");
asylo::EnclaveInput input;
SetEnclaveUserMessage(&input, FLAGS_message);
asylo::EnclaveOutput output;
status = client->EnterAndRun(input, &output);
LOG_IF(QFATAL, !status.ok()) << "EnterAndRun failed with: " << status;
// Финализация
asylo::EnclaveFinal final_input;
status = manager->DestroyEnclave(client, final_input);
LOG_IF(QFATAL, !status.ok()) << "DestroyEnclave failed with: " << status;
return 0;
}
В коде выше показаны три точка входа в анклав. Давайте пройдёмся по каждой части кода.
Инициализация
Недоверенное приложение выполняет следующие шаги для инициализации доверенного:
- Настраивает экземпляр
EnclaveManager
с параметрами по умолчанию.EnclaveManager
обрабатывает все ресурсы анклава в недоверенном приложении. - Настраивает объект
SimLoader
для получения бинарного образа анклава с диска. - Вызывает
EnclaveManager::LoadEnclave
, чтобы привязать анклав к имени"demo enclave"
. Затем неявно вызывается метод анклаваInitialize
.
Безопасное выполнение
Недоверенное приложение выполняет следующие шаги для безопасного выполнения рабочей нагрузки в доверенном приложении:
- Получает дескриптор анклава через
EnclaveManager::GetClient
. - Передаёт произвольные входные данные в
EnclaveInput
. В этом примере используется строковое protobuf-расширение для сообщенияEnclaveInput
. Поле этого расширение используется для передачи данных анклаву для шифрования. - Вызывает анклав с помощью
EnclaveClient::EnterAndRun
. Этот метод является основной точкой входа, используемой для отправки сообщений в анклав. Его можно вызывать произвольное число раз. - Получает результат из анклава в
EnclaveOutput
. Разработчики могут добавить protobuf-расширения к сообщениюEnclaveOutput
для предоставления произвольных выходных значений из их анклава.
Финализация
Недоверенное приложение выполняет следующие шаги для финализации доверенного приложения:
- Передаёт произвольные данные финализации в анклав и уничтожает анклав с помощью
EnclaveManager::DestroyEnclave
. - Запускает метод анклава
Finalize
. Фреймворк Asylo неявно выполняет этот шаг во время уничтожения анклава.
Пишем приложение в анклаве
Мы уже знаем, как инициализировать, запускать и финализировать анклав с помощью Asylo. Эти вызовы происходили на недоверенной стороне анклава. Теперь давайте посмотрим на код на доверенной стороне.
constexpr size_t kMaxMessageSize = 1 << 16;
// Dummy 128-bit AES key.
constexpr uint8_t kAesKey128[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
0x06, 0x07, 0x08, 0x09, 0x10, 0x11,
0x12, 0x13, 0x14, 0x15};
// Encrypts a message against `kAesKey128` and returns a 12-byte nonce followed
// by authenticated ciphertext, encoded as a hex string. `message` must be less
// than or equal to `kMaxMessageSize` in length.
const StatusOr EncryptMessage(const string &message) {
AesGcmSivCryptor cryptor(kMaxMessageSize, new AesGcmSivNonceGenerator());
CleansingVector key(kAesKey128, kAesKey128 + arraysize(kAesKey128));
CleansingString additional_authenticated_data;
CleansingString nonce;
CleansingString ciphertext;
Status status = cryptor.Seal(key, additional_authenticated_data, message,
&nonce, &ciphertext);
if (!status.ok()) {
return status;
}
return absl::BytesToHexString(absl::StrCat(nonce, ciphertext));
}
class EnclaveDemo : public TrustedApplication {
public:
EnclaveDemo() = default;
Status Run(const EnclaveInput &input, EnclaveOutput *output) {
string user_message = GetEnclaveUserMessage(input);
StatusOr result = EncryptMessage(user_message);
if (!result.ok()) {
return result.status();
}
std::cout << "Encrypted message:" << std::endl
<< result.ValueOrDie() << std::endl;
return Status::OkStatus();
}
const string GetEnclaveUserMessage(const EnclaveInput &input) {
return input.GetExtension(guide::asylo::enclave_input_demo).value();
}
};
В коде выше определяется класс EnclaveDemo
, который наследуется от TrustedApplication
, и реализуется логика безопасного выполнения анклава в методе Run
. Этот метод шифрует входное сообщение и выводит зашифрованный текст.
Класс TrustedApplication
предоставляет реализации методов Initialize
, Run
, и Finalize
по умолчанию. Предполагается, что создатель анклава переопределит эти методы должным образом для реализации логики анклава. Как показано в этом примере, создатель анклава обычно переопределяет метод TrustedApplication::Run
для обеспечения анклава основной логикой и использует этот метод для взаимодействия с анклавом. В качестве альтернативы создатель анклава может запустить RPC-сервер (например, gRPC-сервер) в методе TrustedApplication::Initialize
и затем взаимодействовать с анклавом через сервер. В этом случае разработчик может не переопределять метод TrustedApplication::Run
. Фреймворк Asylo гибкий и позволяет разработчикам использовать анклавы наиболее удобным для них способом.
Пишем и запускаем приложение в анклаве
Asylo реализует анклавный бэкенд для среды, основанной на симуляторах. Чтобы написать приложение в анклаве, мы должны объявить несколько объектов, которые будут использовать этот бэкенд.
asylo_proto_library(
name = "demo_proto",
srcs = ["demo.proto"],
deps = ["@com_google_asylo//asylo:enclave_proto"],
)
sim_enclave(
name = "demo_enclave",
srcs = ["demo_enclave.cc"],
deps = [
":demo_proto_cc",
"@com_google_asylo//asylo:enclave_runtime",
],
)
debug_enclave_driver(
name = "quickstart",
srcs = ["demo_driver.cc"],
enclaves = [":demo_enclave"],
deps = [
":demo_proto_cc",
"@com_google_asylo//asylo:enclave_client",
"@com_github_gflags_gflags//:gflags_nothreads",
"@com_google_asylo//asylo/util:logging",
],
Показанный выше файл Bazel BUILD определяет логику нашего анклава в sim_enclave
с именем demo_enclave
. Этот объект содержит нашу реализацию TrustedApplication
и связан с временем выполнения Asylo. Мы используем правило sim_enclave
для создания анклава, который можно запустить в режиме симуляции.
Недоверенным компонентом является demo_driver
, который содержит код для обработки логики инициализации, запуска и финализации анклава, а также отправки и получения сообщений через интерфейс анклава. В приложении вне анклава quickstart
был бы cc_binary
объектом, однако правило debug_enclave_driver
упрощает сочетание объектов драйвера и анклава. В частности, оно гарантирует, что demo_driver.cc
компилируется с помощью crosstool хоста, что зависимости данных анклава компилируются с помощью бэкенд-специфичного crosstool анклава и что demo_driver
вызывается с флагом --enclave_path
, который указывает путь к бинарному образу анклава.
Теперь давайте запустим демо-анклав внутри образа Docker, который мы скачали ранее. Вы можете установить флаг --message
, который передаётся объекту //quickstart
, со строкой, которую вы хотите зашифровать.
Примечание Следующая команда запускает анклав в режиме симуляции.
docker run -it \
-v bazel-cache:/root/.cache/bazel \
-v "${MY_PROJECT}":/opt/my-project \
-w /opt/my-project \
gcr.io/asylo-framework/asylo \
bazel run --config=enc-sim //quickstart -- --message="Asylo Rocks"
Encrypted message:
2dc402068266ba995608e0d4a16c1604b792355d4635dec43cf2888cf2036d2007772ed5f24e5c
Поздравляем, вы написали и запустили своё первое приложение в анклаве!
Что делать дальше
Теперь вы знаете достаточно о Asylo для того, чтобы попробовать изменить приложение в анклаве. Вот что можно попробовать сделать:
- Обратите внимание на то, что на данный момент мы никак не используем переменную
output
, переданную вEnterAndRun
. ИспользуйтеSetEnclaveOutputMessage
вdemo_enclave.cc
иGetEnclaveOutputMessage
вdemo_driver.cc
, чтобы вернуть зашифрованное сообщение из анклава в драйвер и отобразить его. Вывод приложения не должен измениться. - Функцию
EnterAndRun
можно вызывать много раз после инициализации анклава. Изменитеdemo_driver.cc
, чтобы добавить ещё один вызовEnterAndRun
с целью перезайти в анклав с другим сообщением для шифрования. - Используйте protobuf-расширения в сообщении
EnclaveInput
, чтобы добавить возможность отправки зашифрованного сообщения в анклав для дешифровки с помощью функцииDecryptMessage
.
Код доступен на GitHub.