Passbolt CE API авторизация: пошаговое решение ошибки

    При интеграции с Passbolt Community Edition через API разработчики часто сталкиваются с ошибкой «Учетные данные недействительны» на этапе JWT-логина. Проблема кроется в несоответствии fingerprint ключа, переданного в запросе, и того, который сервер ожидает после проверки подписи challenge. В этой статье мы разберём типичную ситуацию, когда используется серверный GPG-ключ вместо ключа пользователя, и покажем, как правильно настроить аутентификацию.

    Почему возникает ошибка «Учетные данные недействительны»?

    Passbolt использует асимметричное шифрование: сервер отправляет challenge (случайную строку, закодированную как fingerprint), клиент должен подписать его своим приватным ключом и отправить обратно. Если fingerprint, который сервер получил при проверке подписи, не совпадает с переданным в теле запроса keyid, авторизация отклоняется. В вашем случае сервер ожидает fingerprint A505F33A1191319E787D0B2E5E92BA8A2C424B49, а вы передаёте fingerprint пользовательского ключа 999AAF9999E99999DC999A99CD99B990DE999999.

    Какой ключ использовать для авторизации в Passbolt API?

    Для корректной работы через API нужно использовать приватный ключ того пользователя, от имени которого вы хотите авторизоваться. Серверный ключ (server_private_key.asc) предназначен для административных задач и расшифровки данных на стороне сервера, но не для JWT-логина от имени конкретного пользователя. Если вы передаёте серверный ключ, то fingerprint совпадает, но сервер не находит соответствующего пользователя - отсюда ошибка.

    Пошаговое исправление: от генерации до подписи

    1. Загрузите правильный приватный ключ пользователя

    Убедитесь, что в переменной окружения PRIVATE_KEY указан путь к ключу конкретного пользователя, а не сервера. Ключ пользователя экспортируется из Passbolt через веб-интерфейс (секция «Мой аккаунт» → «Ключи»).

    2. Расшифруйте ключ, если он защищён паролем

    В вашем коде расшифровка реализована верно: проверяете isDecrypted() и, если нужно, вызываете openpgp.decryptKey с passphrase. Для пользовательских ключей, созданных через интерфейс, passphrase обязателен.

    3. Получите challenge корректно

    Запрос GET /auth/verify.json?username=... возвращает fingerprint, который соответствует публичному ключу пользователя на сервере. Если вы загрузили приватный ключ другого пользователя, fingerprint не совпадёт - это нормально, но нужно убедиться, что username в запросе совпадает с владельцем ключа.

    4. Подпишите challenge тем же ключом

    Функция signChallenge использует расшифрованный ключ для подписи строки challenge. Важно: подпись должна быть detached (откреплённой) и в бинарном формате, как в вашем коде. После подписи конвертируйте результат в base64.

    5. Передайте правильный fingerprint в JWT-запросе

    В теле запроса keyid должен быть равен fingerprint того же ключа, которым вы подписывали. Его можно получить из переменной decryptedKey.getFingerprint() после расшифровки. Не используйте fingerprint из ответа сервера - он может отличаться, если вы ошиблись с пользователем.

    Исправленный пример кода

    const fs = require('fs');
    const openpgp = require('openpgp');
    const axios = require('axios');
    require('dotenv').config();
    
    const PRIVATE_KEY = process.env.PRIVATE_KEY;
    const PASSPHRASE = process.env.PASSPHRASE;
    const EMAIL = process.env.EMAIL;
    const API_URL = process.env.API_URL;
    
    async function loadPrivateKey() {
      console.log('[2] Загружаем приватный ключ...');
      const privateKeyArmored = fs.readFileSync(PRIVATE_KEY, 'utf8');
      const privateKey = await openpgp.readPrivateKey({ armoredKey: privateKeyArmored });
    
      if (privateKey.isDecrypted()) {
        console.log('[INFO] Ключ уже расшифрован');
        return privateKey;
      }
    
      const decryptedKey = await openpgp.decryptKey({ privateKey, passphrase: PASSPHRASE });
      console.log('[INFO] Ключ расшифрован через passphrase');
      return decryptedKey;
    }
    
    async function getChallenge() {
      console.log('[1] Получаем challenge...');
      const res = await axios.get(`${API_URL}/auth/verify.json?username=${encodeURIComponent(EMAIL)}`);
      return res.data.body.fingerprint;
    }
    
    async function signChallenge(challenge, decryptedKey) {
      console.log('[3] Подписываем challenge...');
      const message = await openpgp.createMessage({ text: challenge });
      const detachedSignature = await openpgp.sign({
        message,
        signingKeys: decryptedKey,
        detached: true,
        format: 'binary'
      });
      return Buffer.from(detachedSignature).toString('base64');
    }
    
    async function loginJWT(fingerprint, signatureBase64) {
      console.log('[4] Выполняем JWT авторизацию...');
      try {
        const response = await axios.post(`${API_URL}/auth/jwt/login.json`, {
          gpg_auth: {
            keyid: fingerprint,
            signature: signatureBase64
          }
        });
        console.log('[✅ Успешный ответ]:', response.data);
      } catch (err) {
        if (err.response) {
          console.error('[Ошибка от сервера]:', err.response.data);
        } else {
          console.error('[Ошибка]:', err.message);
        }
      }
    }
    
    (async () => {
      const privateKey = await loadPrivateKey();
      const userFingerprint = privateKey.getFingerprint(); // Получаем fingerprint ключа
      console.log('[INFO] Используемый fingerprint:', userFingerprint);
      const challenge = await getChallenge();
      const signatureBase64 = await signChallenge(challenge, privateKey);
      await loginJWT(userFingerprint, signatureBase64);
    })();

    Ключевое изменение: keyid теперь динамически берётся из расшифрованного ключа через метод getFingerprint(). Это гарантирует, что fingerprint в запросе совпадает с тем, который сервер вычислит при проверке подписи.

    Распространённые ошибки и их решения

    • Ошибка «Ключ уже расшифрован» при использовании серверного ключа - это нормально, так как серверный ключ Passbolt CE часто хранится без passphrase. Не используйте его для пользовательской авторизации.
    • Несовпадение fingerprint после подписи - проверьте, что приватный ключ принадлежит тому же пользователю, чей email указан в EMAIL. Экспортируйте ключ через веб-интерфейс Passbolt.
    • Ошибка 400 с сообщением «Учетные данные недействительны» - чаще всего возникает из-за неверного fingerprint в keyid. Используйте getFingerprint() для автоматического получения.

    Заключение

    Для успешной JWT-авторизации в Passbolt CE через API необходимо использовать приватный ключ того пользователя, от имени которого выполняется запрос, и передавать его fingerprint в теле запроса. Серверный ключ не подходит для этой задачи. Следуя приведённому исправленному коду, вы сможете избежать ошибки «Учетные данные недействительны» и корректно интегрировать Passbolt с вашим приложением.

    Часто задаваемые вопросы