aboutsummaryrefslogtreecommitdiff
path: root/src/auth_flow.cpp
diff options
context:
space:
mode:
authorOsmium Sorcerer <os@sof.beauty>2026-09-23 23:17:50 +0000
committerOsmium Sorcerer <os@sof.beauty>2026-09-23 23:17:50 +0000
commit105a208968bac60a2aafc85f64615ffc18458036 (patch)
tree392a946fdc6c8b4859d5b6f07969a2906c5d0631 /src/auth_flow.cpp
parent9b1903f4b902a4e9d1aa16888f82d8268c2cf4ef (diff)
Rewrite authentication flow to handle new keysHEADmaster
Previously, running /auth command would immediately send off request with the provided username, and all keyring actions will only happen once the client receives the challenge. This isn't possible now that we have both software X25519 and hardware P-256 keys because we must signal which key type we're using in advance. Rather than deferring everything until the challenge, perform the key setup immediately. Parse it, load it into the auth state, and send the full request. When the challenge is received, all that's left is to unlock the already resolved key. In case user cancels the prompt, open key selection dialog again. Make auth state machine stricter, so it's more consistent and easier to reason about, and only expose small interface. In particular, it provides key switching without restarting the auth flow. The state itself is only initialized once at program start, fully static, and avoids superflous allocations and deletions which could result in invalid states. Unify auth-related structures under it that were previously separate, such as saved hostname-username pairs. Send signals about request and response being ready instead of directly using the network functions, which makes it especially better fit in context of two asynchronous dialogs. To properly encode hardware or software key type in the intitial request without creating a separate packet, an extension to subprotocol 2 was introduced in a format-violating manner: an extension bit set means a hardware key is used, and thus we'll expect an ephemeral P-256 key as the challenge. The entire credential message format will be completely rewritten in version 3, this is a prototype implementation.
Diffstat (limited to 'src/auth_flow.cpp')
-rw-r--r--src/auth_flow.cpp276
1 files changed, 194 insertions, 82 deletions
diff --git a/src/auth_flow.cpp b/src/auth_flow.cpp
index 07544e8..8d164de 100644
--- a/src/auth_flow.cpp
+++ b/src/auth_flow.cpp
@@ -3,31 +3,55 @@
#include <QJsonObject>
#include <QTableView>
#include <QVBoxLayout>
+#include <QMessageBox>
+#include <QPushButton>
+#include <QLabel>
#include "auth_flow.h"
-#include "keyring.h"
#include "file_functions.h"
+void AuthFlow::initialize(KeyringModel *model)
+{
+ m_model = model;
+ m_saved_auth.load();
+}
+
// This function is supposed to open the authentication dialog with various
// fields like method selection and fields to enter password or select a key,
-// but for now, it'll simply submit a public key auth request. Hostname
-// parameter is unused.
-void start_auth_flow(AOApplication *ao_app, QString username)
+// but for now, it'll simply submit a public key auth request.
+void AuthFlow::start(QString user, QString host)
{
- ao_app->ex_auth_username = username;
- AuthRequest req;
- req.username = username;
- req.method = AuthMethod::certificate;
- ao_app->send_ex_message(serializeAuthRequest(req));
+ if (m_phase == AuthPhase::requested)
+ {
+ reset();
+ }
+
+ m_username = user;
+ m_hostname = host;
+ m_key_dlg = new KeySelectDialog(m_model, m_username, m_hostname);
+ m_key_dlg->setWindowModality(Qt::ApplicationModal);
+ connect(m_key_dlg, &KeySelectDialog::key_selected, this, &AuthFlow::on_key_selected);
+ connect(m_key_dlg, &QDialog::rejected, this, &AuthFlow::reset);
+
+ auto saved_key_id = m_saved_auth.lookup(host.toUtf8(), user.toUtf8());
+ if (!saved_key_id.isEmpty())
+ {
+ m_mode = FlowMode::Saved;
+ on_key_selected(acquire_keyring_key(saved_key_id));
+ }
+ else
+ {
+ m_mode = FlowMode::Default;
+ m_key_dlg->show();
+ }
}
-KeySelectDialog::KeySelectDialog(KeyringModel *model, QStringView username, QWidget *parent)
- : QDialog(parent)
+KeySelectDialog::KeySelectDialog(KeyringModel *model, QStringView username, QStringView hostname, QWidget *parent)
+ : QDialog(parent)
, m_model(model)
{
- this->setAttribute(Qt::WA_DeleteOnClose);
- this->setWindowTitle(QString("Select key for %1").arg(username));
+ this->setWindowTitle(QString("Select key for %1@%2").arg(username, hostname));
auto view = new QTableView(this);
view->setModel(m_model);
view->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
@@ -46,10 +70,9 @@ KeySelectDialog::KeySelectDialog(KeyringModel *model, QStringView username, QWid
return;
}
- QByteArray key_id = m_model->data(rows.first(), KeyringModel::KeyIDRole).toByteArray();
- QString key_name = m_model->data(rows.first()).toString();
+ QByteArray selected_key_id = m_model->data(rows.first(), KeyringModel::KeyIDRole).toByteArray();
- emit key_selected(key_id, key_name);
+ emit key_selected(acquire_keyring_key(selected_key_id));
});
connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, [view, buttons](const QItemSelection &, const QItemSelection &) {
bool selected = !view->selectionModel()->selectedRows().isEmpty();
@@ -57,15 +80,28 @@ KeySelectDialog::KeySelectDialog(KeyringModel *model, QStringView username, QWid
});
}
-KeyPassphraseDialog::KeyPassphraseDialog(QStringView key_name, QWidget *parent)
- : QDialog(parent)
+KeyActivationDialog::KeyActivationDialog(Mode mode, QStringView key_name, QWidget *parent)
+ : QDialog(parent)
{
- this->setWindowTitle(QString("Enter passphrase for key %1").arg(key_name));
+ this->setWindowTitle(QString("Unlock key %1").arg(key_name));
QVBoxLayout *pw_layout = new QVBoxLayout(this);
- pw_layout->addWidget(new QLabel(QStringLiteral("Passphrase:"), this));
- m_pw_line = new QLineEdit(this);
- m_pw_line->setEchoMode(QLineEdit::Password);
- pw_layout->addWidget(m_pw_line);
+
+ QString mode_prompt;
+ switch (mode) {
+ case Mode::passphrase:
+ mode_prompt = QStringLiteral("Passphrase:");
+ break;
+ case Mode::pin:
+ mode_prompt = QStringLiteral("PIN:");
+ break;
+ default:
+ break;
+ }
+
+ pw_layout->addWidget(new QLabel(mode_prompt, this));
+ m_line = new QLineEdit(this);
+ m_line->setEchoMode(QLineEdit::Password);
+ pw_layout->addWidget(m_line);
QDialogButtonBox *pw_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
pw_layout->addWidget(pw_buttons);
m_err_lbl = new QLabel(this);
@@ -73,87 +109,163 @@ KeyPassphraseDialog::KeyPassphraseDialog(QStringView key_name, QWidget *parent)
pw_layout->addWidget(m_err_lbl);
connect(pw_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(pw_buttons, &QDialogButtonBox::accepted, this, [this] {
- emit passphrase_submitted(m_pw_line->text().toUtf8());
- m_pw_line->clear();
- // zero the buffer
+ emit input_submitted(m_line->text().toUtf8());
+ m_line->clear();
});
}
-void KeyPassphraseDialog::display_error(ResponseResult error) const
+QString auth_error_to_string(ResponseResult error)
{
- QString error_text;
switch (error)
{
case ResponseResult::unsupported_version:
- error_text = QStringLiteral("Unsupported key version.");
- break;
+ return QStringLiteral("Unsupported key version.");
case ResponseResult::decryption_failed:
- error_text = QStringLiteral("Wrong passphrase (or corrupted key).");
- break;
+ return QStringLiteral("Wrong passphrase (or corrupted key).");
+ case ResponseResult::hardware_fault:
+ return QStringLiteral("Hardware rejected this operation. It might be due to corrupted key, invalid server challenge, or a TPM fault.");
+ case ResponseResult::invalid_pin:
+ return QStringLiteral("Invalid PIN.");
+ case ResponseResult::hardware_lockout:
+ return QStringLiteral("Your hardware is locked out after too many failed PIN attempts. Try again later.");
+ case ResponseResult::incompatible_arguments:
+ return QStringLiteral("Server authentication incompatible with your key.");
default:
- error_text = QString("Error unlocking key (code %1). Catastrophic failure.").arg((int)error);
- break;
+ return QString("Error unlocking key (code %1). Catastrophic failure.").arg((int)error);
}
- m_err_lbl->setText(error_text);
+}
+
+void KeyActivationDialog::display_error(ResponseResult error) const
+{
+ m_err_lbl->setText(auth_error_to_string(error));
m_err_lbl->setVisible(true);
}
-AuthFlow::AuthFlow(AOApplication *ao_app, const AuthChallenge &challenge, QWidget *parent)
- : QObject(parent)
- , m_ao_app(ao_app)
- , m_challenge(challenge)
+void AuthFlow::on_key_selected(KeyringKey key)
{
- auto saved_key = ao_app->saved_auth.lookup(ao_app->m_serverdata.m_server_hostname.toUtf8(), ao_app->ex_auth_username.toUtf8());
- if (!saved_key.isEmpty())
+ m_key = key;
+ // Choosing the key for the first request.
+ if (m_phase == AuthPhase::none)
{
- m_mode = FlowMode::Saved;
- m_key_dlg = nullptr;
- on_key_selected(saved_key, QString("%1@%2 (saved)").arg(ao_app->ex_auth_username, ao_app->m_serverdata.m_server_hostname));
+ AuthRequest req = {
+ .username = m_username,
+ .method = AuthMethod::certificate,
+ .variant = m_key.type == KeyringStorageType::hardware ? CertVariant::hardware : CertVariant::software,
+ };
+
+ m_phase = AuthPhase::requested;
+
+ emit request_ready(req);
}
- else
+ // Already authenticating; choosing a different key for the same request.
+ // This can be erroneous if a server challenge is issued for a different type
+ // of key (software X25519 vs hardware P-256), creating a mismatch.
+ else if (m_phase == AuthPhase::requested)
{
- m_mode = FlowMode::Default;
- m_key_dlg = new KeySelectDialog(&ao_app->keyring_model, ao_app->ex_auth_username, parent);
- connect(m_key_dlg, &KeySelectDialog::key_selected, this, &AuthFlow::on_key_selected);
- m_key_dlg->open();
+ respond(m_challenge);
}
}
-void AuthFlow::on_key_selected(QByteArrayView key_id, QStringView key_name)
+void AuthFlow::authenticate(QByteArrayView input)
{
- m_pwd_dlg = new KeyPassphraseDialog(key_name, m_key_dlg);
- connect(m_pwd_dlg, &KeyPassphraseDialog::passphrase_submitted, this, [this, key_id](QByteArrayView passphrase) {
- AuthResponse response;
- ResponseResult result = unlock_and_auth(key_id, passphrase, m_challenge.challenge, m_ao_app->ex_auth_username.toUtf8(), response);
- if (result == ResponseResult::success)
- {
- m_ao_app->send_ex_message(serializeAuthResponse(response));
- if (m_mode == FlowMode::Default)
- {
- m_key_dlg->accept();
- m_ao_app->saved_auth.insert(m_ao_app->m_serverdata.m_server_hostname.toUtf8(), m_ao_app->ex_auth_username.toUtf8(), key_id);
- }
- else
- {
- m_pwd_dlg->accept();
- }
- deleteLater();
- }
- else
+ AuthResponse response;
+ ResponseResult result = unlock_and_auth(m_key, input, m_challenge.challenge, m_username.toUtf8(), response);
+ if (result == ResponseResult::success)
+ {
+ emit response_ready(response);
+ if (m_mode == FlowMode::Default)
{
- m_pwd_dlg->display_error(result);
+ m_saved_auth.insert(m_hostname.toUtf8(), m_username.toUtf8(), m_key.id);
}
- });
- connect(m_pwd_dlg, &QDialog::rejected, this, [this] {
- if (m_mode == FlowMode::Saved)
- {
- m_ao_app->saved_auth.remove(m_ao_app->m_serverdata.m_server_hostname.toUtf8(), m_ao_app->ex_auth_username.toUtf8());
- m_mode = FlowMode::Default;
+ reset();
+ }
+ // The error behavior depends on whether we need the application-level key
+ // unlock dialog (for TSS2) or not (for Windows CNG, which uses its own).
+ else if (m_input_dlg)
+ {
+ m_input_dlg->display_error(result);
+ }
+ else
+ {
+ QMessageBox::warning(nullptr, "Error", auth_error_to_string(result));
+ reject_and_select_new_key();
+ }
+}
- m_key_dlg = new KeySelectDialog(&m_ao_app->keyring_model, m_ao_app->ex_auth_username);
- connect(m_key_dlg, &KeySelectDialog::key_selected, this, &AuthFlow::on_key_selected);
- m_key_dlg->open();
- }
- });
- m_pwd_dlg->open();
+void AuthFlow::reject_and_select_new_key()
+{
+ if (m_mode == FlowMode::Saved)
+ {
+ m_saved_auth.remove(m_hostname.toUtf8(), m_username.toUtf8());
+ m_mode = FlowMode::Default;
+ }
+
+ if (m_input_dlg)
+ {
+ m_input_dlg->deleteLater();
+ }
+
+ // The key selection dialog remains in the memory while the authentication
+ // attempt is performed. It's only cleared on finalization of the entire flow.
+ m_key_dlg->show();
+}
+
+void AuthFlow::call_input_dialog(KeyActivationDialog::Mode mode)
+{
+ m_input_dlg = new KeyActivationDialog(mode, m_key.name);
+ m_input_dlg->setWindowModality(Qt::ApplicationModal);
+
+ connect(m_input_dlg, &KeyActivationDialog::input_submitted, this, &AuthFlow::authenticate);
+ connect(m_input_dlg, &QDialog::rejected, this, &AuthFlow::reject_and_select_new_key);
+ m_key_dlg->hide();
+ m_input_dlg->show();
+}
+
+bool AuthFlow::awaiting_challenge() const
+{
+ return m_phase == AuthPhase::requested;
+}
+
+void AuthFlow::respond(const AuthChallenge &challenge)
+{
+ if (m_phase != AuthPhase::requested)
+ {
+ return;
+ }
+
+ m_challenge = challenge;
+
+ switch (m_key.type) {
+ case KeyringStorageType::software:
+ call_input_dialog(KeyActivationDialog::Mode::passphrase);
+ break;
+ case KeyringStorageType::hardware:
+#ifdef SOF_AO_HARDWARE_KEY_REQUIRES_PIN_UI
+ call_input_dialog(KeyActivationDialog::Mode::pin);
+#else
+ m_key_dlg->hide();
+ authenticate(QByteArray());
+#endif
+ break;
+ default:
+ QMessageBox::warning(nullptr, "Error", QStringLiteral("Unable to use this key."));
+ break;
+ }
+}
+
+void AuthFlow::reset()
+{
+ if (m_key_dlg)
+ {
+ m_key_dlg->deleteLater();
+ m_key_dlg = nullptr;
+ }
+
+ if (m_input_dlg)
+ {
+ m_input_dlg->deleteLater();
+ m_input_dlg = nullptr;
+ }
+
+ m_phase = AuthPhase::none;
}