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 с идентификатором пакета и версией.
  • Я вижу двоичный файл демона в списке "Всегда разрешать доступ этим приложениям" на вкладке "Контроль доступа" элемента пароля в связке ключей Access.app.
  • Если я вручную переключусь на "Разрешить всем приложениям доступ к этому элементу" в Keychain Access, это будет работать. Это, однако, несколько лишает смысла сохранение пароля в цепочке для ключей.
  • Мы попытались поиграться с параметрами, чтобы SecKeychainAddInternetPassword, но это, кажется, не имело никакого значения.
  • Мы попытались явно разблокировать брелок с помощью SecKeychainUnlock()Но, как следует из документации, это кажется излишним.
  • Удаление элемента в Keychain Access.app причины SecKeychainFindInternetPassword() уступать errSecItemNotFound, как и следовало ожидать. Так что он может определенно найти сохраненный элемент, ему просто не разрешено его читать.

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

Есть идеи?

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,

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

Другие вопросы по тегам