Переход от Платформы работы с данными к Платформе конфиденциальных вычислений

Привет, Хабр! Меня зовут Александр, в компании oneFactor я являюсь руководителем платформенных продуктов, и одна из моих задач — это развитие платформы конфиденциальных вычислений. Именно о них мы и будем говорить в данной статье, пройдя путь по преобразованию Платформы работы с данными в Платформу конфиденциальных вычислений над данными, где основной целью является – обеспечение нераскрытия информации, загружаемой в Платформу, даже для администратора Платформы.

Мы будем идти по пути следования данных, начиная от источника, проходя по стадиям преобразований и заканчивая конечной целью – результатом обработки, обеспечивая на всех шагах конфиденциальность данных. Конечно же мы ответим и на вопрос – для чего все это необходимо?

Весь предоставляемый в рамках статьи материал основан на нашем практическом опыте, приобретенном в ходе создания «Платформы совместных конфиденциальных вычислений oneFactor», на которой мы и наши клиенты тренируем и исполняем модели машинного обучения.

В путь

Предположим, у вас есть платформа для работы с данными, которую вы применяете для целей аналитики, управленческой отчетности или машинного обучения. Для такого рода решений данные определяют многое, именно они вносят весомый вклад в ценность конечного продукта для его потребителей. Это означает, что задача обогащения существующих данных дополнительными признаками и детализациями не перестанет быть актуальной никогда.

Мы в компании oneFactor, решая задачу обогащения данных на нашей дата-платформе, применили одно интересное технологическое решение, позволяющее объединять кросс-доменные данные (которые в обычной ситуации не собираются вместе), и это дало в нам дополнительный прирост качества алгоритмов машинного обучения на 20–35%. Одним из ключевых требований при использовании данных было – не раскрывать их содержимого.

Так нами и была создана «Платформа совместных конфиденциальных вычислений». Рассмотрим на примере как пройти этот путь.

Итак, представим, мы имеем хранилище, решаем на нем задачи: обучение моделей, инференс моделей, получение аналитической отчетности.

Схема хранилища выглядит, например, так: 

 
 

 Ставим цель – подключать источники данных

  1. Которые будем применять для обучения моделей

  2. Будем применять для создания новых отчетов

  3. Сохранять полную конфиденциальность данных, не раскрывая их никому, включая администратора платформы

Что входит в список наших задач:

  1. Реализовать подготовку данных на стороне их владельца

  2. Реализовать контроль качества данных в платформе

  3. Реализовать преобразования данных к виду, необходимому потребителю (модель, витрина, отчеты)

  4. Гарантировать владельцу данных их конфиденциальность при использовании

Рассмотрим детальнее каждый из этих пунктов.

Поставка данных

Нам необходимо организовать поставку данных в Платформу и не раскрыть их содержимого. Для подготовки данных мы просто прибегнем к их симметричному шифрованию, при этом ключ, как и положено, оставим только у владельца данных. Предположим, у нас есть исходный датасет вида:

 

Применяем к нему алгоритм шифрования (например, AES GCM 128), на python это будет выглядеть так:

import pandas as pd

df = pd.read_csv('dataset.csv')

# encryption key
key = '7f5c894178f8586e5c1bb436504279a5'

# dataframe with all encrypted columns except id
enc_df = df.apply(lambda x: x if x.name == 'id' else x.apply(lambda y: encrypt_value(key, float(y))))

Где encrypt_value() это:

def encrypt_value(key, value) -> str:
    
    from base64 import b64decode
    from Crypto.Cipher import AES
    import struct
    import os
    
    # set key and value to bytes
    key_bytes = bytes.fromhex(key)
    value_bytes = bytearray(struct.pack('<d', value))

    # set initialization vector
    iv = os.urandom(12)
    
    # encrypt value
    cipher = AES.new(key_bytes, AES.MODE_GCM, iv)
    enc_value, tag = cipher.encrypt_and_digest(value_bytes)
    
    return(enc_value.hex() + tag.hex() + iv.hex())

Получаем датасет вида:

 

Как видите мы не шифровали колонку ID, она нам нужна для таких операций как join с другими датасетами, но зашифровали все остальные колонки. В таком виде и будем данные поставлять в Платформу. Опустим описание того, как данные будут загружаться в Платформу, подразумеваем, что эта возможность уже поддерживается.

Регулярность поставки данных может быть любой – ежемесячной, еженедельной, ежедневной, ежечасной и т. д. Это определяется бизнес-задачей.

Загрузке данных в историю будет посвящена отдельная статья. Пока лишь ограничусь вот чем – владелец данных в праве менять ключ шифрования для каждой поставляемой партиции данных, поэтому хранение исторических зашифрованных данных – это задача, требующая отдельной проработки. 

Контроль качества данных

Отдельно остановимся на контроле качества данных. Датасет зашифрован, ключ находится у владельца данных, что можно в нем проверить? Да, без дешифрования данных мы не сможем обеспечить стопроцентный контроль качества. Тогда зачем их шифровать? Ответы на эти вопросы даны в разделе «Обеспечение конфиденциальности данных», на данном этапе научимся работать с зашифрованными данными, понимая, что ключ и данные в открытом виде на Платформе будут доступны только алгоритму.

Не буду детально расписывать проверки, которые можно сделать на открытых частях поставляемых данных (например, проверить количество, дубли по ID, возможность джойна по ID другими датасетами, и т. д.), перейду сразу к контролю качества зашифрованной части.

Один из способов проверить зашифрованное содержимое – это посчитать PSI (Population stability index, в сети можно найти много статей на эту тему) на сэмпле, выделенном из общего датасета. К слову сказать, этот способ работает только в том случае, если датасет от поставки к поставке однороден. Предположим, что это так, тогда наш план действий будет следующим:

  • Выделяем сэмпл из датасета.

  • Дешифруем зашифрованную часть сэмпла.

  • Получаем распределение.

  • Считаем PSI.

На python это будет выглядеть так:

# extract sample
enc_sample = enc_df.sample(n=100000)

# decrypt all encrypted columns in sample dataframe
sample = enc_sample.apply(lambda x: x if x.name == 'id' else x.apply(lambda y: decrypt_value(key, y)))

# example of default bounds and distribution for 'value1' column
bounds = [0.05616942, 0.547390152, 0.432641311, 0.26431151, 0.20872376, 0.596734102, 0.652678437, 0.715995908, 0.825436966]
distribution = [0.12, 0.083, 0.083, 0.13, 0.083, 0.083, 0.14, 0.083, 0.083, 0.11]

# calculate psi for 'value1' column
calculate_psi(sample, 'value1', bounds, distribution)

 Где decrypt_value() это: 

def decrypt_value(key, enc_string) -> float:
    
    from base64 import b64decode
    from Crypto.Cipher import AES
    import struct

    # set key to bytes
    key_bytes = bytes.fromhex(key)

    # get value, tag and initialization vector
    enc_value = bytes.fromhex(enc_string[:16])
    tag = bytes.fromhex(enc_string[-56:-24])
    iv = bytes.fromhex(enc_string[-24:])

    # decrypt value
    cipher = AES.new(key_bytes, AES.MODE_GCM, iv)
    value_bytes = cipher.decrypt_and_verify(enc_value, tag)
    
    return(struct.unpack('<d', value_bytes)[0])

А calculate_psi() это:

def calculate_psi(dataframe, column_name, default_bounds, default_distribution) -> float:
    import math

    # calculate distribution
    prev_item = min(default_bounds) - 1
    distribution = []
    default_bounds.sort()
    for item in default_bounds:
        distribution += [len(dataframe[(dataframe[column_name] > prev_item) & (dataframe[column_name] <= item)]) / len(dataframe)]
        prev_item = item

    distribution += [len(dataframe[(dataframe[column_name] > item)]) / len(dataframe)]

    # calculate psi
    psi_indexes = []
    for i in range(0, len(distribution), 1):
        if (distribution[i] == 0):
            distribution[i] = 0.0000001
        psi_indexes += [(distribution[i] - default_distribution[i]) * math.log(distribution[i] / default_distribution[i])]

    return(sum(psi_indexes))

Полученный результат интерпретируем так:

psi < 0.1 – Данные прошли проверку качества.

psi >= 0.1 и < 0.25 – Возможно требуется исследование ситуации.

psi >= 0.25 – Данные не прошли проверку качества, загрузка без исправления невозможна.

Обработка данных

Рассмотрим два примера, в которых мы получим конечный бизнес-результат их обработки:

  1. Расчет статистики.

  2. Инференс модели машинного обучения.

Описывая примеры, будем держать в уме, что в конечном решении ключ шифрования и данные в открытом виде на Платформе будут доступны только алгоритмам.

Сформулируем первую задачу – необходимо получить статистику вида среднее значение по группам записей, группы хранятся в Платформе в открытом виде отдельно от защищенных данных, при этом возможно их объединение по ID. Для расчета мы просто объединим два датасета по ID и рассчитаем avg. На python пример для трех колонок выглядит так:

# read groups
groups_df = pd.read_csv('groups.csv')

# decrypt all encrypted columns in dataframe
data_df = enc_df.apply(lambda x: x if x.name == 'id' else x.apply(lambda y: decrypt_value(key, y)))

# merge and calculate avg
avg = data_df.merge(groups_df, how='left', on=['id']).groupby(by=['group_name'])['value1', 'value2', 'value3'].mean()

Результат будет выглядеть так:

 

Вторая задача – необходимо применить заранее обученную модель к выборке, по которой мы должны собрать признаки из загруженного датасета и уже имеющиеся признаки на Платформе. Объединяем эти данные и вызываем модель. На python это выглядит так:

# read platform features dataset
features_df = pd.read_csv('features.csv')

# decrypt all encrypted columns in dataframe
data_df = enc_df.apply(lambda x: x if x.name == 'id' else x.apply(lambda y: decrypt_value(key, y)))

# merge into tds
tds = data_df.merge(features_df, how='left', on=['id'])

# example to collect model features list
features = [row for row in tds.columns if 'id' not in row]

# predict
model = lgb.Booster(model_file = 'model.txt')
scored_tds = tds[['id']].copy()
scored_tds['score'] = model.predict(tds[features])

Как видите, мы решили поставленные задачи, научившись работать с зашифрованными данными. Первая часть пути пройдена, теперь обеспечим защиту и нераскрытие поставляемых в Платформу данных.

Обеспечение конфиденциальности данных

Вот мы и подошли к самой интересной части данной статьи, отвечающей на вопрос – как обеспечить полную конфиденциальность данных, которые их владельцы поставляют в Платформу? Уточню – речь идет не о том, как не допустить взлома Платформы и утечки данных, речь идет о том, как защитить данные от доступа к ним пользователя с правами администратора. То есть ставим себе цель – не видеть данные и доказать их владельцу, что это физически невозможно сделать, то есть будем проводить конфиденциальные вычисления с данными без доступа человека к ним.

Для этого нам необходимы:

1. Технология

Одно из существующих промышленных программно-аппаратных решений, позволяющих выполнять конфиденциальные вычисления

2. Защищенное приложение

Приложение, реализующее алгоритмы работы с данными, но не раскрывающее сами данные 

3. Непосредственно само доказательство

Аттестация приложения владельцем данных, включающая подтверждение его подлинности, безопасность алгоритмов, обрабатывающих конфиденциальные данные

Технология конфиденциальных вычислений

Существует несколько промышленных программно-аппаратных решений для разработки приложений, выполняющих конфиденциальные вычисления. Аппаратная поддержка позволяет избежать хранения данных в памяти в открытом виде в процессе вычислений, ведь, обладая правами администратора, можно эти данные из памяти прочитать. А наша цель – полная конфиденциальность. Так у Intel и AMD есть линейки процессоров, поддерживающих конфиденциальные вычисления. Мы остановили свой выбор на технологии Intel SGX и используем для этого решения на процессорах Ice Lake с поддержкой технологии SGX 2.0.

Программная часть технологии Intel SGX представлена решениями, позволяющими как разрабатывать нативные приложения, так и запускать в защищенной среде ранее разработанные приложения.

Защищенное приложение

В примерах выше мы обрабатывали зашифрованные данные обладая ключом, которым мы могли эти данные расшифровать, что само по себе – нонсенс. Но только не в нашем случае, когда мы сделали это осознанно в качестве промежуточного шага на пути к созданию платформы конфиденциальных вычислений. Теперь наконец мы обсудим среду исполнения, в которой доступ к ключу шифрования и данным (в открытом виде) имеют только алгоритмы, но не человек. Средой исполнения в нашем случае будет являться – защищенное приложение, разработанное с применением технологии Intel SGX.

Для создания такого приложения необходимо, по сути, перенести в него алгоритмы работы с данными такие как мы описывали выше и добавить пару вспомогательных функций. Приложение должно поддерживать:

  1. Загрузку зашифрованных данных.

  2. Поставку ключа шифрования.

  3. Исполнение алгоритмов работы с данными и возврат результата либо в открытом и агрегированном виде, либо в зашифрованном виде.

Это все что нам нужно. Я не упоминаю нефункциональные требования, но нужно иметь в виду, что для продакшенализации нам понадобятся такие возможности как логирование, мониторинг и т. д. (а это тема отдельной статьи)

В соответствии с требованиями структура приложения должна выглядеть так:

 

Это все, что должно уметь наше защищенное приложение.

При этом не столь важно какой из подходов будет выбран к его реализации – нативное/существующее приложение, важно то, что при применении технологии секреты – данные и ключ шифрования будут под надежной защитой.

Как доказать владельцу сохранность данных?

Вопрос, который задаст владелец данных, перед тем как поставить сами данные и ключ в Платформу – каким образом будет гарантирована сохранность данных (от всех, в том числе от администратора)?

Технология Intel SGX гарантирует, что разработанное с ее помощью приложение, запущенное в SGX-среде, будет обеспечивать сохранность переданных ему секретов. Соответственно перед тем, как поставить секреты в Платформу, их владелец должен убедиться в:

  1. Их безопасности на всем пути поставки в защищенное приложение.

  2. Надежной защите секретов внутри защищенного приложения.

  3. Достоверности алгоритмов приложения, обрабатывающих данные.

Технология Intel SGX предоставляет возможность удаленной аттестации защищенных приложений, в рамках которой пользователь (в нашем случае владелец данных) получает подтверждение в том, что:

  • Приложение – это действительно SGX-приложение.

  • Приложение запущено в SGX-среде.

  • Устанавливает TLS-соединение с анклавом (анклав – защищенная часть SGX-приложения) для передачи в него секретов.

И это дает ответ на первые два пункта. Что касается третьего пункта – достоверности алгоритмов, то тут подоплека в следующем – предположим, некий алгоритм получает зашифрованные данные на вход и отдает открытые эти же данные на выход. При такой реализации даже «честное» SGX-приложение, запущенное в «честной» SGX-среде, может раскрывать данные, что владельца данных совершенно не устроит. Для решения этой задачи существует два подхода:

  1. Проверка Identity самого приложения. То есть перед тем, как поставить секреты в него, владелец данных убеждается в том, что он взаимодействует с определенной версией/сборкой, в которой используется алгоритм, выполняющий разрешенные действия с данными.

  2. Проверка подписи алгоритмов, приложение запускает только подписанные владельцем данных алгоритмы.

Оба этих подхода сопровождаются аудитом исходных кодов защищенного приложения со стороны владельца данных.

Теперь мы концептуально готовы к переходу к конфиденциальным вычислениям, обеспечить как полную сохранность данных, так и получать все бенефиты от их применения на нашей Платформе.

Заключение

Если вы решили проделать этот путь, то необходимо понимать, работы предстоит немало – реализация, продакшенализация, получение и тестирование первых бизнес-результатов, поиск новых подходов к работе с конфиденциальными данными, масштабирование, но стоит отметить – результат того стоит, вы сможете извлекать из данных дополнительную пользу.

Со своей стороны скажу, что для отдельных частей данной статьи мы планируем опубликовать несколько более подробных статей, отвечающих на вопрос – «как реализовать определенные шаги преобразования?» (надеюсь, это будет интересно).

В завершение приведу архитектуру решения, которая у нас получилась на выходе:

Статьи с Хабра