QDnsLookup: add support for TLSA records

[ChangeLog][QtNetwork][QDnsLookup] Added support for querying records of
type TLSA, which are useful in DNS-based Authentication of Named
Entities (DANE).

Change-Id: I455fe22ef4ad4b2f9b01fffd17c723aa6ab7f278
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
Thiago Macieira 2024-04-17 11:09:58 -07:00
parent 503fd60988
commit 4503dabfbd
8 changed files with 398 additions and 5 deletions

View File

@ -124,6 +124,11 @@
\title RFC 6724
*/
/*!
\externalpage https://datatracker.ietf.org/doc/html/rfc6698
\title RFC 6698
*/
/*!
\externalpage https://datatracker.ietf.org/doc/html/rfc7049
\title RFC 7049

View File

@ -257,6 +257,8 @@ static void qt_qdnsservicerecord_sort(QList<QDnsServiceRecord> &records)
\value SRV service records.
\value[since 6.8] TLSA TLS association records.
\value TXT text records.
*/
@ -704,6 +706,21 @@ QList<QDnsTextRecord> QDnsLookup::textRecords() const
return d_func()->reply.textRecords;
}
/*!
\since 6.8
Returns the list of TLS association records associated with this lookup.
According to the standards relating to DNS-based Authentication of Named
Entities (DANE), this field should be ignored and must not be used for
verifying the authentity of a given server if the authenticity of the DNS
reply cannot itself be confirmed. See isAuthenticData() for more
information.
*/
QList<QDnsTlsAssociationRecord> QDnsLookup::tlsAssociationRecords() const
{
return d_func()->reply.tlsAssociationRecords;
}
#if QT_CONFIG(ssl)
/*!
\since 6.8
@ -1261,6 +1278,223 @@ QDnsTextRecord &QDnsTextRecord::operator=(const QDnsTextRecord &other)
very fast and never fails.
*/
/*!
\class QDnsTlsAssociationRecord
\since 6.8
\brief The QDnsTlsAssociationRecord class stores information about a DNS TLSA record.
\inmodule QtNetwork
\ingroup network
\ingroup shared
When performing a text lookup, zero or more records will be returned. Each
record is represented by a QDnsTlsAssociationRecord instance.
The meaning of the fields is defined in \l{RFC 6698}.
\sa QDnsLookup
*/
QT_DEFINE_QSDP_SPECIALIZATION_DTOR(QDnsTlsAssociationRecordPrivate)
/*!
\enum QDnsTlsAssociationRecord::CertificateUsage
This enumeration contains valid values for the certificate usage field of
TLS Association queries. The following list is up-to-date with \l{RFC 6698}
section 2.1.1 and RFC 7218 section 2.1. Please refer to those documents for
authoritative instructions on interpreting this enumeration.
\value CertificateAuthorityConstrait
Indicates the record includes an association to a specific Certificate
Authority that must be found in the TLS server's certificate chain and
must pass PKIX validation.
\value ServiceCertificateConstraint
Indicates the record includes an association to a certificate that must
match the end entity certificate provided by the TLS server and must
pass PKIX validation.
\value TrustAnchorAssertion
Indicates the record includes an association to a certificate that MUST
be used as the ultimate trust anchor to validate the TLS server's
certificate and must pass PKIX validation.
\value DomainIssuedCertificate
Indicates the record includes an association to a certificate that must
match the end entity certificate provided by the TLS server. PKIX
validation is not tested.
\value PrivateUse
No standard meaning applied.
\value PKIX_TA
Alias; mnemonic for Public Key Infrastructure Trust Anchor
\value PKIX_EE
Alias; mnemonic for Public Key Infrastructure End Entity
\value DANE_TA
Alias; mnemonic for DNS-based Authentication of Named Entities Trust Anchor
\value DANE_EE
Alias; mnemonic for DNS-based Authentication of Named Entities End Entity
\value PrivCert
Alias
Other values are currently reserved, but may be unreserved by future
standards. This enumeration can be used for those values even if no
enumerator is provided.
\sa certificateUsage()
*/
/*!
\enum QDnsTlsAssociationRecord::Selector
This enumeration contains valid values for the selector field of TLS
Association queries. The following list is up-to-date with \l{RFC 6698}
section 2.1.2 and RFC 7218 section 2.2. Please refer to those documents for
authoritative instructions on interpreting this enumeration.
\value FullCertificate
Indicates this record refers to the full certificate in its binary
structure form.
\value SubjectPublicKeyInfo
Indicates the record refers to the certificate's subject and public
key information, in DER-encoded binary structure form.
\value PrivateUse
No standard meaning applied.
\value Cert
Alias
\value SPKI
Alias
\value PrivSel
Alias
Other values are currently reserved, but may be unreserved by future
standards. This enumeration can be used for those values even if no
enumerator is provided.
\sa selector()
*/
/*!
\enum QDnsTlsAssociationRecord::MatchingType
This enumeration contains valid values for the matching type field of TLS
Association queries. The following list is up-to-date with \l{RFC 6698}
section 2.1.3 and RFC 7218 section 2.3. Please refer to those documents for
authoritative instructions on interpreting this enumeration.
\value Exact
Indicates this the certificate or SPKI data is stored verbatim in this
record.
\value Sha256
Indicates this a SHA-256 checksum of the the certificate or SPKI data
present in this record.
\value Sha512
Indicates this a SHA-512 checksum of the the certificate or SPKI data
present in this record.
\value PrivateUse
No standard meaning applied.
\value PrivMatch
Alias
Other values are currently reserved, but may be unreserved by future
standards. This enumeration can be used for those values even if no
enumerator is provided.
\sa matchingType()
*/
/*!
Constructs an empty TLS Association record.
*/
QDnsTlsAssociationRecord::QDnsTlsAssociationRecord()
: d(new QDnsTlsAssociationRecordPrivate)
{
}
/*!
Constructs a copy of \a other.
*/
QDnsTlsAssociationRecord::QDnsTlsAssociationRecord(const QDnsTlsAssociationRecord &other) = default;
/*!
Moves the content of \a other into this object.
*/
QDnsTlsAssociationRecord &
QDnsTlsAssociationRecord::operator=(const QDnsTlsAssociationRecord &other) = default;
/*!
Destroys this TLS Association record object.
*/
QDnsTlsAssociationRecord::~QDnsTlsAssociationRecord() = default;
/*!
Returns the name of this record.
*/
QString QDnsTlsAssociationRecord::name() const
{
return d->name;
}
/*!
Returns the duration in seconds for which this record is valid.
*/
quint32 QDnsTlsAssociationRecord::timeToLive() const
{
return d->timeToLive;
}
/*!
Returns the certificate usage field for this record.
*/
QDnsTlsAssociationRecord::CertificateUsage QDnsTlsAssociationRecord::usage() const
{
return d->usage;
}
/*!
Returns the selector field for this record.
*/
QDnsTlsAssociationRecord::Selector QDnsTlsAssociationRecord::selector() const
{
return d->selector;
}
/*!
Returns the match type field for this record.
*/
QDnsTlsAssociationRecord::MatchingType QDnsTlsAssociationRecord::matchType() const
{
return d->matchType;
}
/*!
Returns the binary data field for this record. The interpretation of this
binary data depends on the three numeric fields provided by
certificateUsage(), selector(), and matchType().
Do note this is a binary field, even for the checksums, similar to what
QCyrptographicHash::result() returns.
*/
QByteArray QDnsTlsAssociationRecord::value() const
{
return d->value;
}
static QDnsLookupRunnable::EncodedLabel encodeLabel(const QString &label)
{
QDnsLookupRunnable::EncodedLabel::value_type rootDomain = u'.';

View File

@ -22,8 +22,11 @@ class QDnsHostAddressRecordPrivate;
class QDnsMailExchangeRecordPrivate;
class QDnsServiceRecordPrivate;
class QDnsTextRecordPrivate;
class QDnsTlsAssociationRecordPrivate;
class QSslConfiguration;
QT_DECLARE_QSDP_SPECIALIZATION_DTOR(QDnsTlsAssociationRecordPrivate)
class Q_NETWORK_EXPORT QDnsDomainNameRecord
{
public:
@ -138,6 +141,78 @@ private:
Q_DECLARE_SHARED(QDnsTextRecord)
class Q_NETWORK_EXPORT QDnsTlsAssociationRecord
{
Q_GADGET
public:
enum class CertificateUsage : quint8 {
// https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#certificate-usages
// RFC 6698
CertificateAuthorityConstrait = 0,
ServiceCertificateConstraint = 1,
TrustAnchorAssertion = 2,
DomainIssuedCertificate = 3,
PrivateUse = 255,
// Aliases by RFC 7218
PKIX_TA = 0,
PKIX_EE = 1,
DANE_TA = 2,
DANE_EE = 3,
PrivCert = 255,
};
Q_ENUM(CertificateUsage)
enum class Selector : quint8 {
// https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#selectors
// RFC 6698
FullCertificate = 0,
SubjectPublicKeyInfo = 1,
PrivateUse = 255,
// Aliases by RFC 7218
Cert = FullCertificate,
SPKI = SubjectPublicKeyInfo,
PrivSel = PrivateUse,
};
Q_ENUM(Selector)
enum class MatchingType : quint8 {
// https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#matching-types
// RFC 6698
Exact = 0,
Sha256 = 1,
Sha512 = 2,
PrivateUse = 255,
PrivMatch = PrivateUse,
};
Q_ENUM(MatchingType)
QDnsTlsAssociationRecord();
QDnsTlsAssociationRecord(const QDnsTlsAssociationRecord &other);
QDnsTlsAssociationRecord(QDnsTlsAssociationRecord &&other)
: d(std::move(other.d))
{}
QDnsTlsAssociationRecord &operator=(QDnsTlsAssociationRecord &&other) noexcept { swap(other); return *this; }
QDnsTlsAssociationRecord &operator=(const QDnsTlsAssociationRecord &other);
~QDnsTlsAssociationRecord();
void swap(QDnsTlsAssociationRecord &other) noexcept { d.swap(other.d); }
QString name() const;
quint32 timeToLive() const;
CertificateUsage usage() const;
Selector selector() const;
MatchingType matchType() const;
QByteArray value() const;
private:
QSharedDataPointer<QDnsTlsAssociationRecordPrivate> d;
friend class QDnsLookupRunnable;
};
Q_DECLARE_SHARED(QDnsTlsAssociationRecord)
class Q_NETWORK_EXPORT QDnsLookup : public QObject
{
Q_OBJECT
@ -178,6 +253,7 @@ public:
NS = 2,
PTR = 12,
SRV = 33,
TLSA = 52,
TXT = 16
};
Q_ENUM(Type)
@ -230,7 +306,7 @@ public:
QList<QDnsDomainNameRecord> pointerRecords() const;
QList<QDnsServiceRecord> serviceRecords() const;
QList<QDnsTextRecord> textRecords() const;
QList<QDnsTlsAssociationRecord> tlsAssociationRecords() const;
#if QT_CONFIG(ssl)
void setSslConfiguration(const QSslConfiguration &sslConfiguration);

View File

@ -57,6 +57,7 @@ public:
QList<QDnsDomainNameRecord> nameServerRecords;
QList<QDnsDomainNameRecord> pointerRecords;
QList<QDnsServiceRecord> serviceRecords;
QList<QDnsTlsAssociationRecord> tlsAssociationRecords;
QList<QDnsTextRecord> textRecords;
#if QT_CONFIG(ssl)
@ -296,6 +297,15 @@ public:
QList<QByteArray> values;
};
class QDnsTlsAssociationRecordPrivate : public QDnsRecordPrivate
{
public:
QDnsTlsAssociationRecord::CertificateUsage usage;
QDnsTlsAssociationRecord::Selector selector;
QDnsTlsAssociationRecord::MatchingType matchType;
QByteArray value;
};
QT_END_NAMESPACE
#endif // QDNSLOOKUP_P_H

View File

@ -406,6 +406,23 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply)
if (status < 0)
return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid service record"));
reply->serviceRecords.append(record);
} else if (type == QDnsLookup::TLSA) {
// https://datatracker.ietf.org/doc/html/rfc6698#section-2.1
if (size < 3)
return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid TLS association record"));
const quint8 usage = response[offset];
const quint8 selector = response[offset + 1];
const quint8 matchType = response[offset + 2];
QDnsTlsAssociationRecord record;
record.d->name = name;
record.d->timeToLive = ttl;
record.d->usage = QDnsTlsAssociationRecord::CertificateUsage(usage);
record.d->selector = QDnsTlsAssociationRecord::Selector(selector);
record.d->matchType = QDnsTlsAssociationRecord::MatchingType(matchType);
record.d->value.assign(response + offset + 3, response + offset + size);
reply->tlsAssociationRecords.append(std::move(record));
} else if (type == QDnsLookup::TXT) {
QDnsTextRecord record;
record.d->name = name;

View File

@ -224,6 +224,25 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply)
record.d->timeToLive = ptr->dwTtl;
record.d->weight = ptr->Data.Srv.wWeight;
reply->serviceRecords.append(record);
} else if (ptr->wType == QDnsLookup::TLSA) {
// Note: untested, because the DNS_RECORD reply appears to contain
// no records relating to TLSA. Maybe WinDNS filters them out of
// zones without DNSSEC.
QDnsTlsAssociationRecord record;
record.d->name = name;
record.d->timeToLive = ptr->dwTtl;
const auto &tlsa = ptr->Data.Tlsa;
const quint8 usage = tlsa.bCertUsage;
const quint8 selector = tlsa.bSelector;
const quint8 matchType = tlsa.bMatchingType;
record.d->usage = QDnsTlsAssociationRecord::CertificateUsage(usage);
record.d->selector = QDnsTlsAssociationRecord::Selector(selector);
record.d->matchType = QDnsTlsAssociationRecord::MatchingType(matchType);
record.d->value.assign(tlsa.bCertificateAssociationData,
tlsa.bCertificateAssociationData + tlsa.bCertificateAssociationDataLength);
reply->tlsAssociationRecords.append(std::move(record));
} else if (ptr->wType == QDnsLookup::TXT) {
QDnsTextRecord record;
record.d->name = name;

View File

@ -374,6 +374,14 @@ QStringList tst_QDnsLookup::formatReply(const QDnsLookup *lookup) const
result.append(std::move(entry));
}
for (const QDnsTlsAssociationRecord &rr : lookup->tlsAssociationRecords()) {
QString entry = u"TLSA %1 %2 %3 %4"_s.arg(int(rr.usage())).arg(int(rr.selector()))
.arg(int(rr.matchType())).arg(rr.value().toHex().toUpper());
if (rr.name() != domain)
entry = "TLSA unexpected label to "_L1 + rr.name();
result.append(std::move(entry));
}
result.sort();
return result;
}
@ -504,6 +512,10 @@ void tst_QDnsLookup::lookup_data()
"SRV 2 50 7 aaaa-single;"
"SRV 3 50 7 a-multi";
QTest::newRow("tlsa") << QDnsLookup::Type::TLSA << "_25._tcp.multi"
<< "TLSA 3 1 1 0123456789ABCDEFFEDCBA9876543210"
"0123456789ABCDEFFEDCBA9876543210";
QTest::newRow("txt-single") << QDnsLookup::TXT << "txt-single"
<< "TXT \"Hello\"";
QTest::newRow("txt-multi-onerr") << QDnsLookup::TXT << "txt-multi-onerr"
@ -522,8 +534,12 @@ void tst_QDnsLookup::lookup()
if (!lookup)
return;
QCOMPARE(lookup->error(), QDnsLookup::NoError);
#ifdef Q_OS_WIN
if (QTest::currentDataTag() == "tlsa"_L1)
QSKIP("WinDNS doesn't work properly with TLSA records and we don't know why");
#endif
QCOMPARE(lookup->errorString(), QString());
QCOMPARE(lookup->error(), QDnsLookup::NoError);
QCOMPARE(lookup->type(), type);
QCOMPARE(lookup->name(), domainName(domain));

View File

@ -37,10 +37,18 @@ static QDnsLookup::Type typeFromString(QString str)
return QDnsLookup::Type(value);
}
template <typename Enum> [[maybe_unused]] static const char *enumToKey(Enum e)
template <typename Enum> QByteArray enumToString(Enum value)
{
QMetaEnum me = QMetaEnum::fromType<Enum>();
return me.valueToKey(int(e));
QByteArray keys = me.valueToKeys(int(value));
if (keys.isEmpty())
return QByteArrayLiteral("<unknown>");
// return the last one
qsizetype idx = keys.lastIndexOf('|');
if (idx > 0)
return std::move(keys).sliced(idx + 1);
return keys;
}
static int showHelp(const char *argv0, int exitcode)
@ -113,6 +121,14 @@ static void printAnswers(const QDnsLookup &lookup)
printf("%s ", qPrintable(QDebug::toString(data)));
puts("");
}
for (const QDnsTlsAssociationRecord &rr : lookup.tlsAssociationRecords()) {
printRecordCommon(rr, "TLSA");
printf("( %u %u %u ; %s %s %s\n\t%s )\n", quint8(rr.usage()), quint8(rr.selector()),
quint8(rr.matchType()), enumToString(rr.usage()).constData(),
enumToString(rr.selector()).constData(), enumToString(rr.matchType()).constData(),
rr.value().toHex().toUpper().constData());
}
}
static void printResults(const QDnsLookup &lookup, QElapsedTimer::Duration duration)
@ -143,7 +159,7 @@ static void printResults(const QDnsLookup &lookup, QElapsedTimer::Duration durat
#if QT_CONFIG(ssl)
if (lookup.nameserverProtocol() != QDnsLookup::Standard) {
if (QSslConfiguration conf = lookup.sslConfiguration(); !conf.isNull()) {
printf(" (%s %s)", enumToKey(conf.sessionProtocol()),
printf(" (%s %s)", enumToString(conf.sessionProtocol()).constData(),
qPrintable(conf.sessionCipher().name()));
}
}