Mac Launch Daemon не может получить пароль из системной связки ключей после сохранения его там

У нас есть Launch Daemon, который (обязательно по разным причинам) запускается от имени пользователя root и взаимодействует с серверным компонентом по сети. Ему необходимо пройти аутентификацию в службе, поэтому, когда он впервые получает пароль, мы сохраняем его в системной связке ключей. При последующих запусках идея состоит в том, чтобы получить пароль из связки ключей и использовать его для аутентификации с помощью сетевой службы.

Это работало нормально, но в macOS 10.12 существующий код перестал работать, и мы были полностью озадачены тем, как это исправить. Все сводится к следующему:

Независимо от того, сохраняем ли мы новый пароль или получаем старый, мы получаем ссылку на системную связку ключей, используя это:

SecKeychainCopyDomainDefault(kSecPreferencesDomainSystem, &system_keychain);

Мы также для удобства отключаем взаимодействие с пользователем, хотя мы ожидаем, что оно уже отключено в контексте демона.

SecKeychainSetUserInteractionAllowed(false);

При сохранении нового пароля в связке ключей мы используем

OSStatus status = SecKeychainAddInternetPassword(
    system_keychain,
    urlLength, server_base_url,
    0, NULL,
    usernameLength, username,
    0, NULL,
    0,
    kSecProtocolTypeAny, kSecAuthenticationTypeAny,
    passwordLength, password,
    NULL);

Это много работает. Сообщается об успехе, и я вижу элемент в «системной» связке ключей в Keychain Access.app.

Получение его при последующих запусках нашего демона выполняется с помощью этой строки:

status = SecKeychainFindInternetPassword(
    system_keychain,
    urlLength, url,
    0, NULL,
    usernameLength, username,
    0, NULL,
    0,
    kSecProtocolTypeAny, kSecAuthenticationTypeAny,
    &passwordLength, &password_data,
    NULL);

К сожалению, он начал возвращать errSecAuthFailed по неясным нам причинам.

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

  • Бинарный файл демона подписан сертификатом Developer Id.
  • Бинарный файл демона содержит встроенный раздел Info.plist с идентификатором пакета и версией.
  • Я вижу двоичный файл демона в списке «Всегда разрешать доступ этим приложениям» на вкладке «Контроль доступа» элемента пароля в Keychain Access.app.
  • Если я вручную переключаюсь на «Разрешить всем приложениям доступ к этому элементу» в Keychain Access, он работает. Однако это несколько лишает смысла сохранение пароля в связке ключей.
  • Мы пробовали поиграть с параметрами SecKeychainAddInternetPassword, но, похоже, это не имело никакого значения.
  • Мы пробовали явно разблокировать связку ключей с помощью SecKeychainUnlock(), но, как предполагает документация, это кажется излишним.
  • Удаление элемента в Keychain Access.app приводит к тому, что SecKeychainFindInternetPassword() дает errSecItemNotFound, как и следовало ожидать. Таким образом, он определенно может найти сохраненный элемент, но ему просто не разрешено его читать.

Документацию связки ключей не совсем легко читать, а отчасти она довольно тавтологична. («Чтобы сделать Y, вам нужно сделать Y», не говоря уже о том, почему вы хотите сделать Y.) Тем не менее, я думаю, что я прошел через это и понял большую часть из этого. Различные аспекты нашей конкретной настройки не рассматриваются подробно (доступ от демона), но кажется довольно очевидным, что доступ к элементу, ранее сохраненному тем же приложением, не требует какой-либо специальной авторизации или аутентификации. Это прямо противоречит тому поведению, которое мы наблюдаем.

Любые идеи?


person pmdj    schedule 16.11.2016    source источник


Ответы (1)


Потратив на это еще несколько часов в течение нескольких дней, мы наконец поняли, что происходит.

Во-первых, я попытался создать минимальный пример, воспроизводящий проблему. Это не сработало с errSecAuthFailed и, следовательно, не воспроизвело проблему. Итак, вернемся к исходному демону, должно быть что-то конкретное в нем, что пошло не так.

Следующей идеей было проверить в системном журнале время вызова SecKeychainFindInternetPassword(). Появилось несколько сообщений об ошибках:

securityd   CSSM Exception: -2147411889 CSSMERR_CL_UNKNOWN_TAG
securityd   MacOS error: -67063
securityd   MacOS error: -67063
securityd   code requirement check failed (-67063), client is not Apple-signed
securityd   CSSM Exception: 32 CSSM_ERRCODE_OPERATION_AUTH_DENIED
OurDaemon   subsystem: com.apple.securityd, category: security_exception, enable_level: 0, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 0, privacy_setting: 2, enable_private_data: 0
OurDaemon   CSSM Exception: -2147416032 CSSMERR_CSP_OPERATION_AUTH_DENIED

Это наводит на мысль, что проблема может быть в подписи кода. Странный. Проверка подписи кода двоичного файла с помощью codesign -vv не дала никаких результатов.

После поиска в Интернете различных частей сообщений об ошибках я нашел -67063 соответствует errSecCSGuestInvalid. Комментарий гласит: «Идентификатор кода был признан недействительным».

Ладно, определенно какая-то ошибка кодовой подписи, но что это значит и почему возникла?

Поиски еще кое-что наконец привели к объяснению, а также к решению: http://lists.apple.com/archives/apple-cdsa/2010/Mar/msg00027.html

Это означает, что в какой-то момент с момента запуска программы с ней произошло что-то, что сделало ее недействительной.

а также

если вы запустите подписанную программу, затем замените ее (например, создав новую версию на месте :-), а затем запустите новую версию, ядро ​​по-прежнему будет содержать старую подпись, прикрепленную к исполняемый файл vnode. Если это ваша ситуация, просто удаление исполняемого файла и его воссоздание устраняет проблему навсегда (пока вы снова не перезапишете файл :-). Мы рекомендуем всегда заменять подписанный код (mv (1), а не cp (1) или эквиваленты).

Это объяснило это. Я копировал новые версии демона на место, используя

sudo cp path/to/built/daemon /usr/local/libexec/

По-видимому, это перезаписывает файл на месте, а не создает новый vnode, записывает его и затем переименовывает его поверх старого файла. Таким образом, решение состоит в том, чтобы сначала cp во временный каталог, а затем mv на место. Или удалите целевой файл перед использованием cp.

Как только я это сделал, все заработало!

person pmdj    schedule 20.11.2016