Comments 8
А вариант exec(sha256sum argv[0]) с перехватом stdout в таком варианте не прокатит? Как самый простой вариант?
И да - реализовывать лень, да и не на чем. Очень уж задачка "для плохих собеседований", если такой алгоритм устроит.
App Attest + Notarization Ticket + SecCode API
1. DCAppAttestService (macOS 14+)
Публичный API, не требует отключения SIP. Привязывает ключ в Secure Enclave к конкретному App ID (Team ID + Bundle ID) и выдаёт сертификатную цепочку, подписанную Apple
let service = DCAppAttestService.shared
let keyId = try await service.generateKey()
let attestation = try await service.attestKey(keyId, clientDataHash: SHA256(challenge))2. Notarization Ticket как доказательство хеша бинарника
При нотаризации Apple выдаёт тикет, который:
криптографически подписан Apple,
содержит cdHash нотаризованного бинарника,
стаплируется в bundle (
xcrun stapler staple).
Тикет можно извлечь из bundle и отправить удалённой стороне, она верифицирует подпись Apple и извлекает хеш. Публичный ключ Apple для проверки тикетов встроен в macOS и задокументирован. Notarization Ticket нужно уметь парсить (формат Apple Binary Property List внутри .dmg-style структуры). Формат не документирован официально, но хорошо изучен если поискать.
3. SecCode API для получения cdHash запущенного процесса
var code: SecCode?
SecCodeCopySelf([], &code) // code текущего процесса
var info: CFDictionary?
SecCodeCopySigningInformation(code as! SecStaticCode,
SecCSFlags(rawValue: kSecCSSigningInformation),
&info)
let cdHash = (info as! [String: Any])[kSecCodeInfoCdHash as String]
// cdHash — SHA-1 или SHA-256 кодовой подписи, тот же, что в notarization ticketЭто системный вызов, а не self-report приложения - ядро macOS возвращает данные из code signature structure, и подделать их без отключения SIP нельзя
Вся цепочка выглядит как-то так
[Remote Server] [MyBox App]
| |
|-- 1. challenge_nonce --------------->|
| |
| 2. attestKey(keyId, SHA256(nonce))
| → Apple attestation cert chain
| |
| 3. SecCodeCopySelf → cdHash
| Extract notarization ticket from bundle
| |
| 4. payload = {nonce, cdHash, ticket, timestamp}
| assertion = generateAssertion(keyId, SHA256(payload))
| |
|<-- 5. {attestation, payload, assertion}--|
|
| 6. Verify attestation chain (Apple CA)
| 7. Verify assertion signature (attested pubkey)
| 8. Verify notarization ticket (Apple signature)
| 9. ticket.cdHash == payload.cdHash == expected_hashУдалённую аттестацию приложения на macOS можно попытаться собрать из стандартных механизмов системы, не отключая SIP и не используя приватные API.
Идея такая - связать Secure Enclave, cdHash запущенного бинарника, Apple notarization ticket и nonce сервера. Устройство подписывает эти данные ключом из Secure Enclave, сервер проверяет подпись, ticket Apple и совпадение хеша бинарника.
Общая схема
Нужно доказать серверу три факта:
сообщение подписано реальным устройством
подпись относится к конкретному запуску приложения
бинарник соответствует нотариализованной версии Apple
Для этого используются четыре элемента:
ключ устройства в Secure Enclave
cdHash запущенного приложения
notarization ticket Apple
nonce, присланный сервером
1. Ключ устройства (Secure Enclave)
При первом запуске приложение создаёт ключ ECDSA P-256 внутри Secure Enclave.
Через:
Security.frameworkили
DeviceCheck / App Attest
Ключ:
неэкспортируемый
привязан к устройству
используется только для подписи
Серверу отправляется только публичный ключ.
2. Получение хеша запущенного приложения
В рантайме приложение может получить собственный cdHash (CodeDirectory hash) через публичный SecCode API.
var code: SecCode?
SecCodeCopySelf([], &code)
var info: CFDictionary?
SecCodeCopySigningInformation(code!, SecCSFlags(kSecCSSigningInformation), &info)
let dict = info as! [String: Any]
let cdHashes = dict[kSecCodeInfoCdHashes as String] as! [Data]
let cdHash = cdHashes.first!
Это тот же хеш, который macOS использует при проверке подписи приложения.
3. Подтверждение Apple (notarization)
Бинарник проходит обычную цепочку:
codesign
notarization
stapler
После этого внутри .app появляется notarization ticket, подписанный Apple и содержащий cdHash.
Проверка локально:
xcrun stapler validate MyBox.app
4. Протокол аттестации
Сервер отправляет устройству случайный challenge:
nonce
Приложение формирует payload:
payload = nonce || cdHash || ticket || timestamp
И подписывает его ключом Secure Enclave:
signature = ECDSA_sign(payload)
Ответ серверу:
{
"payload": "...",
"signature": "...",
"cdHash": "...",
"ticket": "...",
"devicePublicKey": "..."
}
5. Проверка на сервере
Сервер выполняет три проверки.
1. Ticket Apple
проверяется подпись Apple
извлекается cdHash
2. Совпадение хеша
cdHash_ticket == cdHash_client
3. Подпись устройства
Verify(devicePublicKey, payload, signature)
Также проверяются nonce и timestamp (защита от replay).
Результат
Если все проверки проходят, сервер знает:
сообщение подписано Secure Enclave конкретного Mac
подпись относится к реальному запуску приложения
бинарник соответствует нотариализованной версии Apple
Ограничение
Нужен Secure Enclave - старые Intel-Mac без T2 такой сценарий не поддерживают.
Но эта реализация подвержена следующим рискам ИБ:
Реализация даёт гарантию - сервер может быть уверен, что:
подпись сделана ключом Secure Enclave;
бинарник соответствует нотариализованной версии;
сообщение свежее.
Но не может на 100% доказать, что:
код не модифицирован в памяти;
нет инжектированного кода;
устройство — настоящий Mac;
клиент не проксируется.
Завершили поиск ответа, все предложенные решения собрали в материал: https://habr.com/ru/articles/1009826/comments/
И итоговая гипотеза, которую команда будет тестировать, тоже там :)
Ищем решение для удаленной аттестации приложений на macOS (приз — Mac Mini)