Skip to content

Commit

Permalink
WIP: ssh-agent: Implement destination constraints
Browse files Browse the repository at this point in the history
This change implements loading ssh-agent destination constraints from
KeeAgent.settings into the ssh-agent. For now there is no UI so
configuration must be done in KeePass2/KeeAgent.

The ssh-agent constrain extension is described at [1]. However, I found
it partly misleading:
- in the constaint array each constraint is enveloped where in the
  keyspec arrays the keyspec are just appended to the constraint.
- each constraint and host has an additional string field reserved for
  future use.
The actual structure has been obtained from openssh ssh-add source code [2].

TODO:
- fix TODOs
- error handling
- tests

[1]: https://www.openssh.com/agent-restrict.html
[2]: https://github.com/openssh/openssh-portable/blob/3ad669f81aabbd2ba9fbd472903f680f598e1e99/authfd.c#L538

Signed-off-by: Konrad Gräfe <kgraefe@paktolos.net>
  • Loading branch information
kgraefe committed Feb 9, 2024
1 parent 9259151 commit 4edabff
Show file tree
Hide file tree
Showing 5 changed files with 331 additions and 1 deletion.
156 changes: 156 additions & 0 deletions src/sshagent/KeeAgentSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ KeeAgentSettings::KeeAgentSettings()
reset();
}

bool KeeAgentSettings::KeySpec::operator==(const KeeAgentSettings::KeySpec& other) const
{
return (keyBlob == other.keyBlob && isCertificateAuthority == other.isCertificateAuthority);
}

bool KeeAgentSettings::DestinationConstraint::operator==(const KeeAgentSettings::DestinationConstraint& other) const
{
return (fromHost == other.fromHost && fromHostKeys == other.fromHostKeys && toUser == other.toUser
&& toHost == other.toHost && toHostKeys == other.toHostKeys);
}

bool KeeAgentSettings::operator==(const KeeAgentSettings& other) const
{
// clang-format off
Expand All @@ -43,6 +54,8 @@ bool KeeAgentSettings::operator==(const KeeAgentSettings& other) const
&& m_useConfirmConstraintWhenAdding == other.m_useConfirmConstraintWhenAdding
&& m_useLifetimeConstraintWhenAdding == other.m_useLifetimeConstraintWhenAdding
&& m_lifetimeConstraintDuration == other.m_lifetimeConstraintDuration
&& m_useDestinationConstraintsWhenAdding == other.m_useDestinationConstraintsWhenAdding
&& m_destinationConstraints == other.m_destinationConstraints
&& m_selectedType == other.m_selectedType
&& m_attachmentName == other.m_attachmentName
&& m_saveAttachmentToTempFile == other.m_saveAttachmentToTempFile
Expand Down Expand Up @@ -77,6 +90,8 @@ void KeeAgentSettings::reset()
m_useConfirmConstraintWhenAdding = false;
m_useLifetimeConstraintWhenAdding = false;
m_lifetimeConstraintDuration = 600;
m_useDestinationConstraintsWhenAdding = false;
m_destinationConstraints.clear();

m_selectedType = QStringLiteral("file");
m_attachmentName.clear();
Expand Down Expand Up @@ -125,6 +140,16 @@ int KeeAgentSettings::lifetimeConstraintDuration() const
return m_lifetimeConstraintDuration;
}

bool KeeAgentSettings::useDestinationConstraintsWhenAdding() const
{
return m_useDestinationConstraintsWhenAdding;
}

QList<KeeAgentSettings::DestinationConstraint> KeeAgentSettings::destinationConstraints() const
{
return m_destinationConstraints;
}

const QString KeeAgentSettings::selectedType() const
{
return m_selectedType;
Expand Down Expand Up @@ -180,6 +205,16 @@ void KeeAgentSettings::setLifetimeConstraintDuration(int lifetimeConstraintDurat
m_lifetimeConstraintDuration = lifetimeConstraintDuration;
}

void KeeAgentSettings::setUseDestinationConstraintsWhenAdding(bool useDestinationConstraintsWhenAdding)
{
m_useDestinationConstraintsWhenAdding = useDestinationConstraintsWhenAdding;
}

void KeeAgentSettings::setDestinationConstraints(const QList<DestinationConstraint>& destinationConstraints)
{
m_destinationConstraints = destinationConstraints;
}

void KeeAgentSettings::setSelectedType(const QString& selectedType)
{
m_selectedType = selectedType;
Expand Down Expand Up @@ -229,6 +264,8 @@ bool KeeAgentSettings::fromXml(const QByteArray& ba)
QXmlStreamReader reader;
reader.addData(ba);

reset();

if (reader.error() || !reader.readNextStartElement()) {
m_error = reader.errorString();
return false;
Expand Down Expand Up @@ -273,6 +310,78 @@ bool KeeAgentSettings::fromXml(const QByteArray& ba)
reader.skipCurrentElement();
}
}
if (!reader.error())
reader.readNext();
} else if (reader.name() == "UseDestinationConstraintWhenAdding") {
m_useDestinationConstraintsWhenAdding = readBool(reader);
} else if (reader.name() == "DestinationConstraints") {
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "Constraint") {
KeeAgentSettings::DestinationConstraint constraint;
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "FromHostKeys" || reader.name() == "ToHostKeys") {
QString section = reader.name().toString();
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "KeySpec") {
KeeAgentSettings::KeySpec keyspec;
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "HostKey") {
reader.readNext();
keyspec.keyBlob = reader.text().toString();
reader.readNext();
} else if (reader.name() == "IsCA") {
keyspec.isCertificateAuthority = readBool(reader);
} else {
qWarning() << "Skipping KeySpec element" << reader.name();
reader.skipCurrentElement();
}
}

// TODO: check for valid keyspec?
if (section == "FromHostKeys") {
constraint.fromHostKeys.append(std::move(keyspec));
} else {
constraint.toHostKeys.append(std::move(keyspec));
}
if (!reader.error())
reader.readNext();
} else {
qWarning() << "Skipping " << section << " element" << reader.name();
reader.skipCurrentElement();
}
}
if (!reader.error())
reader.readNext();
} else if (reader.name() == "FromHost") {
reader.readNext();
constraint.fromHost = reader.text().toString();
reader.readNext();
} else if (reader.name() == "ToUser") {
reader.readNext();
constraint.toUser = reader.text().toString();
reader.readNext();
} else if (reader.name() == "ToHost") {
reader.readNext();
constraint.toHost = reader.text().toString();
reader.readNext();
} else {
qWarning() << "Skipping Constraint element" << reader.name();
reader.skipCurrentElement();
}
}

// TODO: check validity?
m_destinationConstraints.append(std::move(constraint));

if (!reader.error())
reader.readNext();
} else {
qWarning() << "Skipping DestinationConstraints element" << reader.name();
reader.skipCurrentElement();
}
}
if (!reader.error())
reader.readNext();
} else {
qWarning() << "Skipping element" << reader.name();
reader.skipCurrentElement();
Expand Down Expand Up @@ -309,6 +418,53 @@ QByteArray KeeAgentSettings::toXml() const
writer.writeTextElement("UseConfirmConstraintWhenAdding", m_useConfirmConstraintWhenAdding ? "true" : "false");
writer.writeTextElement("UseLifetimeConstraintWhenAdding", m_useLifetimeConstraintWhenAdding ? "true" : "false");
writer.writeTextElement("LifetimeConstraintDuration", QString::number(m_lifetimeConstraintDuration));
writer.writeTextElement("UseDestinationConstraintWhenAdding",
m_useDestinationConstraintsWhenAdding ? "true" : "false");

writer.writeStartElement("DestinationConstraints");

foreach (const auto& constraint, m_destinationConstraints) {
writer.writeStartElement("Constraint");

if (constraint.fromHost.isEmpty()) {
writer.writeEmptyElement("FromHost");
} else {
writer.writeTextElement("FromHost", constraint.fromHost);
}

writer.writeStartElement("FromHostKeys");
foreach (const auto& key, constraint.fromHostKeys) {
writer.writeStartElement("KeySpec");
writer.writeTextElement("HostKey", key.keyBlob);
writer.writeTextElement("IsCA", key.isCertificateAuthority ? "true" : "false");
writer.writeEndElement(); // KeySpec
}
writer.writeEndElement(); // FromHostKeys

if (constraint.toUser.isEmpty()) {
writer.writeEmptyElement("ToUser");
} else {
writer.writeTextElement("ToUser", constraint.toUser);
}
if (constraint.toHost.isEmpty()) {
writer.writeEmptyElement("ToHost");
} else {
writer.writeTextElement("ToHost", constraint.toHost);
}

writer.writeStartElement("ToHostKeys");
foreach (const auto& key, constraint.toHostKeys) {
writer.writeStartElement("KeySpec");
writer.writeTextElement("HostKey", key.keyBlob);
writer.writeTextElement("IsCA", key.isCertificateAuthority ? "true" : "false");
writer.writeEndElement(); // KeySpec
}
writer.writeEndElement(); // ToHostKeys

writer.writeEndElement(); // Constraint
}

writer.writeEndElement(); // DestinationConstraints

writer.writeStartElement("Location");
writer.writeTextElement("SelectedType", m_selectedType);
Expand Down
25 changes: 25 additions & 0 deletions src/sshagent/KeeAgentSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ class QXmlStreamReader;
class KeeAgentSettings
{
public:
struct KeySpec
{
QString keyBlob;
bool isCertificateAuthority;

bool operator==(const KeySpec& other) const;
};

struct DestinationConstraint
{
QString fromHost;
QList<KeySpec> fromHostKeys;
QString toUser;
QString toHost;
QList<KeySpec> toHostKeys;

bool operator==(const DestinationConstraint& other) const;
};

KeeAgentSettings();
bool operator==(const KeeAgentSettings& other) const;
bool operator!=(const KeeAgentSettings& other) const;
Expand Down Expand Up @@ -58,6 +77,8 @@ class KeeAgentSettings
bool useConfirmConstraintWhenAdding() const;
bool useLifetimeConstraintWhenAdding() const;
int lifetimeConstraintDuration() const;
bool useDestinationConstraintsWhenAdding() const;
QList<DestinationConstraint> destinationConstraints() const;

const QString selectedType() const;
const QString attachmentName() const;
Expand All @@ -71,6 +92,8 @@ class KeeAgentSettings
void setUseConfirmConstraintWhenAdding(bool useConfirmConstraintWhenAdding);
void setUseLifetimeConstraintWhenAdding(bool useLifetimeConstraintWhenAdding);
void setLifetimeConstraintDuration(int lifetimeConstraintDuration);
void setUseDestinationConstraintsWhenAdding(bool useDestinationConstraintsWhenAdding);
void setDestinationConstraints(const QList<DestinationConstraint>& destinationConstraints);

void setSelectedType(const QString& type);
void setAttachmentName(const QString& attachmentName);
Expand All @@ -87,6 +110,8 @@ class KeeAgentSettings
bool m_useConfirmConstraintWhenAdding;
bool m_useLifetimeConstraintWhenAdding;
int m_lifetimeConstraintDuration;
bool m_useDestinationConstraintsWhenAdding;
QList<DestinationConstraint> m_destinationConstraints;

// location
QString m_selectedType;
Expand Down
59 changes: 59 additions & 0 deletions src/sshagent/SSHAgent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co
request.writeString(securityKeyProvider());
}

if (settings.useDestinationConstraintsWhenAdding()) {
request.write(SSH_AGENT_CONSTRAIN_EXTENSION);
request.writeString(QString("restrict-destination-v00@openssh.com"));
encodeDestinationConstraints(settings.destinationConstraints(), request);
}

QByteArray responseData;
if (!sendMessage(requestData, responseData)) {
return false;
Expand All @@ -322,6 +328,10 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co
m_error += "\n" + tr("A confirmation request is not supported by the agent (check options).");
}

if (settings.useDestinationConstraintsWhenAdding()) {
m_error += "\n" + tr("Destination constraints are invalid or not supported by the agent (check options).");
}

if (isSecurityKey) {
m_error +=
"\n" + tr("Security keys are not supported by the agent or the security key provider is unavailable.");
Expand All @@ -336,6 +346,55 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co
return true;
}

bool SSHAgent::encodeDestinationConstraints(const QList<KeeAgentSettings::DestinationConstraint>& constraints,
BinaryStream& out)
{
QByteArray data;
BinaryStream stream(&data);

foreach (const auto& constraint, constraints) {
encodeDestinationConstraint(constraint, stream);
}

out.writeString(data);
return true;
}

bool SSHAgent::encodeDestinationConstraint(const KeeAgentSettings::DestinationConstraint& constraint, BinaryStream& out)
{
QByteArray data;
BinaryStream stream(&data);

encodeDestinationConstraintHost("", constraint.fromHost, constraint.fromHostKeys, stream);
encodeDestinationConstraintHost(constraint.toUser, constraint.toHost, constraint.toHostKeys, stream);
stream.writeString(QString("")); // reserved

out.writeString(data);
return true;
}

bool SSHAgent::encodeDestinationConstraintHost(const QString user,
const QString hostname,
const QList<KeeAgentSettings::KeySpec>& keys,
BinaryStream& out)
{
QByteArray data;
BinaryStream stream(&data);

stream.writeString(user);
stream.writeString(hostname);
stream.writeString(QString("")); // reserved

foreach (const auto& key, keys) {
auto keyBlob = QByteArray::fromBase64(key.keyBlob.split(" ").last().toLatin1(), QByteArray::Base64Encoding);
stream.writeString(keyBlob);
stream.write(static_cast<quint8>(key.isCertificateAuthority));
}

out.writeString(data);
return true;
}

/**
* Remove an identity from the SSH agent.
*
Expand Down
10 changes: 9 additions & 1 deletion src/sshagent/SSHAgent.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@

#include <QHash>

#include "KeeAgentSettings.h"
#include "OpenSSHKey.h"

class KeeAgentSettings;
class Database;

class SSHAgent : public QObject
Expand Down Expand Up @@ -88,6 +88,14 @@ public slots:
const quint32 AGENT_COPYDATA_ID = 0x804e50ba;
#endif

bool encodeDestinationConstraints(const QList<KeeAgentSettings::DestinationConstraint>& constraints,
BinaryStream& out);
bool encodeDestinationConstraint(const KeeAgentSettings::DestinationConstraint& constraint, BinaryStream& out);
bool encodeDestinationConstraintHost(const QString user,
const QString hostname,
const QList<KeeAgentSettings::KeySpec>& keys,
BinaryStream& out);

QHash<OpenSSHKey, QPair<QUuid, bool>> m_addedKeys;
QString m_error;
};
Expand Down

0 comments on commit 4edabff

Please sign in to comment.