Asylo от Google: как работать с новым фреймворком

Как мы писали ранее, 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;
}

В коде выше показаны три точка входа в анклав. Давайте пройдёмся по каждой части кода.

Инициализация

Недоверенное приложение выполняет следующие шаги для инициализации доверенного:

  1. Настраивает экземпляр EnclaveManager с параметрами по умолчанию. EnclaveManager обрабатывает все ресурсы анклава в недоверенном приложении.
  2. Настраивает объект SimLoader для получения бинарного образа анклава с диска.
  3. Вызывает EnclaveManager::LoadEnclave, чтобы привязать анклав к имени "demo enclave". Затем неявно вызывается метод анклава Initialize.

Безопасное выполнение

Недоверенное приложение выполняет следующие шаги для безопасного выполнения рабочей нагрузки в доверенном приложении:

  1. Получает дескриптор анклава через EnclaveManager::GetClient.
  2. Передаёт произвольные входные данные в EnclaveInput. В этом примере используется строковое protobuf-расширение для сообщения EnclaveInput. Поле этого расширение используется для передачи данных анклаву для шифрования.
  3. Вызывает анклав с помощью EnclaveClient::EnterAndRun. Этот метод является основной точкой входа, используемой для отправки сообщений в анклав. Его можно вызывать произвольное число раз.
  4. Получает результат из анклава в EnclaveOutput. Разработчики могут добавить protobuf-расширения к сообщению EnclaveOutput для предоставления произвольных выходных значений из их анклава.

Финализация

Недоверенное приложение выполняет следующие шаги для финализации доверенного приложения:

  1. Передаёт произвольные данные финализации в анклав и уничтожает анклав с помощью EnclaveManager::DestroyEnclave.
  2. Запускает метод анклава 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 предоставляет реализации методов InitializeRun, и 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.

Перевод статьи «Asylo Quickstart Guide»

Ещё интересное для вас:
Тест: что вы знаете о работе мозга?
Что посмотреть и куда сходить разработчку — ближайшие события