From 58c9c4b60991d2665aef29c5981591391524e108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Nordheim?= Date: Mon, 7 Jan 2019 18:01:36 +0100 Subject: Ssl: Add support for IP-address in alternate subject name While it's not common it still occurs, perhaps especially with 127.0.0.1 Can be tested by attempting to connect to https://1.1.1.1/ using Qt. Change-Id: Idad56476597ab570b8347236ff700fa66ab5b1f4 Fixes: QTBUG-71828 Reviewed-by: Timur Pocheptsov --- src/network/ssl/qasn1element_p.h | 1 + src/network/ssl/qssl.cpp | 3 ++ src/network/ssl/qssl.h | 3 +- src/network/ssl/qsslcertificate_openssl.cpp | 40 ++++++++++++++++++---- src/network/ssl/qsslcertificate_qt.cpp | 28 +++++++++++++-- src/network/ssl/qsslsocket.cpp | 13 +++++++ .../ssl/qsslsocket/certs/subjectAltNameIP.crt | 20 +++++++++++ .../auto/network/ssl/qsslsocket/tst_qsslsocket.cpp | 11 ++++++ 8 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 tests/auto/network/ssl/qsslsocket/certs/subjectAltNameIP.crt diff --git a/src/network/ssl/qasn1element_p.h b/src/network/ssl/qasn1element_p.h index 59d1f58482..22948e3ca5 100644 --- a/src/network/ssl/qasn1element_p.h +++ b/src/network/ssl/qasn1element_p.h @@ -138,6 +138,7 @@ public: Rfc822NameType = 0x81, DnsNameType = 0x82, UniformResourceIdentifierType = 0x86, + IpAddressType = 0x87, // context specific Context0Type = 0xA0, diff --git a/src/network/ssl/qssl.cpp b/src/network/ssl/qssl.cpp index 6b5dbdfeac..c9fa7f85d9 100644 --- a/src/network/ssl/qssl.cpp +++ b/src/network/ssl/qssl.cpp @@ -99,6 +99,9 @@ Q_LOGGING_CATEGORY(lcSsl, "qt.network.ssl"); \value DnsEntry A DNS host name entry; the entry contains a host name entry that the certificate is valid for. The entry may contain wildcards. + \value IpAddressEntry An IP address entry; the entry contains an IP address + entry that the certificate is valid for, introduced in Qt 5.13. + \note In Qt 4, this enum was called \c {AlternateNameEntryType}. That name is deprecated in Qt 5. diff --git a/src/network/ssl/qssl.h b/src/network/ssl/qssl.h index 5c25e4e105..42c7b5c56d 100644 --- a/src/network/ssl/qssl.h +++ b/src/network/ssl/qssl.h @@ -68,7 +68,8 @@ namespace QSsl { enum AlternativeNameEntryType { EmailEntry, - DnsEntry + DnsEntry, + IpAddressEntry }; #if QT_DEPRECATED_SINCE(5,0) diff --git a/src/network/ssl/qsslcertificate_openssl.cpp b/src/network/ssl/qsslcertificate_openssl.cpp index 57a4dca08f..899c8a0d2d 100644 --- a/src/network/ssl/qsslcertificate_openssl.cpp +++ b/src/network/ssl/qsslcertificate_openssl.cpp @@ -44,6 +44,8 @@ #include "qsslkey_p.h" #include "qsslcertificateextension_p.h" +#include + #if QT_CONFIG(thread) #include #endif @@ -207,10 +209,14 @@ QMultiMap QSslCertificate::subjectAlter STACK_OF(GENERAL_NAME) *altNames = (STACK_OF(GENERAL_NAME) *)q_X509_get_ext_d2i( d->x509, NID_subject_alt_name, nullptr, nullptr); + auto altName = [](ASN1_IA5STRING *ia5, int len) { + const char *altNameStr = reinterpret_cast(q_ASN1_STRING_get0_data(ia5)); + return QString::fromLatin1(altNameStr, len); + }; if (altNames) { for (int i = 0; i < q_sk_GENERAL_NAME_num(altNames); ++i) { const GENERAL_NAME *genName = q_sk_GENERAL_NAME_value(altNames, i); - if (genName->type != GEN_DNS && genName->type != GEN_EMAIL) + if (genName->type != GEN_DNS && genName->type != GEN_EMAIL && genName->type != GEN_IPADD) continue; int len = q_ASN1_STRING_length(genName->d.ia5); @@ -219,12 +225,32 @@ QMultiMap QSslCertificate::subjectAlter continue; } - const char *altNameStr = reinterpret_cast(q_ASN1_STRING_get0_data(genName->d.ia5)); - const QString altName = QString::fromLatin1(altNameStr, len); - if (genName->type == GEN_DNS) - result.insert(QSsl::DnsEntry, altName); - else if (genName->type == GEN_EMAIL) - result.insert(QSsl::EmailEntry, altName); + switch (genName->type) { + case GEN_DNS: + result.insert(QSsl::DnsEntry, altName(genName->d.ia5, len)); + break; + case GEN_EMAIL: + result.insert(QSsl::EmailEntry, altName(genName->d.ia5, len)); + break; + case GEN_IPADD: { + QHostAddress ipAddress; + switch (len) { + case 4: // IPv4 + ipAddress = QHostAddress(qFromBigEndian(*reinterpret_cast(genName->d.iPAddress->data))); + break; + case 16: // IPv6 + ipAddress = QHostAddress(reinterpret_cast(genName->d.iPAddress->data)); + break; + default: // Unknown IP address format + break; + } + if (!ipAddress.isNull()) + result.insert(QSsl::IpAddressEntry, ipAddress.toString()); + break; + } + default: + break; + } } q_OPENSSL_sk_pop_free((OPENSSL_STACK*)altNames, reinterpret_cast(q_GENERAL_NAME_free)); diff --git a/src/network/ssl/qsslcertificate_qt.cpp b/src/network/ssl/qsslcertificate_qt.cpp index 2bd5b3b412..cce59b5ef3 100644 --- a/src/network/ssl/qsslcertificate_qt.cpp +++ b/src/network/ssl/qsslcertificate_qt.cpp @@ -50,6 +50,8 @@ #include "qasn1element_p.h" #include +#include +#include QT_BEGIN_NAMESPACE @@ -403,10 +405,32 @@ bool QSslCertificatePrivate::parse(const QByteArray &data) QDataStream nameStream(sanElem.value()); QAsn1Element nameElem; while (nameElem.read(nameStream)) { - if (nameElem.type() == QAsn1Element::Rfc822NameType) { + switch (nameElem.type()) { + case QAsn1Element::Rfc822NameType: subjectAlternativeNames.insert(QSsl::EmailEntry, nameElem.toString()); - } else if (nameElem.type() == QAsn1Element::DnsNameType) { + break; + case QAsn1Element::DnsNameType: subjectAlternativeNames.insert(QSsl::DnsEntry, nameElem.toString()); + break; + case QAsn1Element::IpAddressType: { + QHostAddress ipAddress; + QByteArray ipAddrValue = nameElem.value(); + switch (ipAddrValue.length()) { + case 4: // IPv4 + ipAddress = QHostAddress(qFromBigEndian(*reinterpret_cast(ipAddrValue.data()))); + break; + case 16: // IPv6 + ipAddress = QHostAddress(reinterpret_cast(ipAddrValue.data())); + break; + default: // Unknown IP address format + break; + } + if (!ipAddress.isNull()) + subjectAlternativeNames.insert(QSsl::IpAddressEntry, ipAddress.toString()); + break; + } + default: + break; } } } diff --git a/src/network/ssl/qsslsocket.cpp b/src/network/ssl/qsslsocket.cpp index 8d3ca092ff..68de9dedaa 100644 --- a/src/network/ssl/qsslsocket.cpp +++ b/src/network/ssl/qsslsocket.cpp @@ -2882,6 +2882,19 @@ QSharedPointer QSslSocketPrivate::sslContext(QSslSocket *socket) bool QSslSocketPrivate::isMatchingHostname(const QSslCertificate &cert, const QString &peerName) { + QHostAddress hostAddress(peerName); + if (!hostAddress.isNull()) { + const auto subjectAlternativeNames = cert.subjectAlternativeNames(); + const auto ipAddresses = subjectAlternativeNames.equal_range(QSsl::AlternativeNameEntryType::IpAddressEntry); + + for (auto it = ipAddresses.first; it != ipAddresses.second; it++) { + if (QHostAddress(*it).isEqual(hostAddress, QHostAddress::StrictConversion)) + return true; + } + + return false; + } + const QString lowerPeerName = QString::fromLatin1(QUrl::toAce(peerName)); const QStringList commonNames = cert.subjectInfo(QSslCertificate::CommonName); diff --git a/tests/auto/network/ssl/qsslsocket/certs/subjectAltNameIP.crt b/tests/auto/network/ssl/qsslsocket/certs/subjectAltNameIP.crt new file mode 100644 index 0000000000..1377fbbabb --- /dev/null +++ b/tests/auto/network/ssl/qsslsocket/certs/subjectAltNameIP.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgIURWaTvdnvU+Y+gPSONs61cMCH8JUwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5vcmcwHhcNMTkwMTA4MTExMDMxWhcNMTkw +MjA3MTExMDMxWjAWMRQwEgYDVQQDDAtleGFtcGxlLm9yZzCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBALf0qv9vl8RqvDHpEWfjum7DMrY8qrQnD77C/9f/ +Jl0Jo4UZiSBr1OYYVbiWJyodw8LpQQsKE+fQCo2STb5X9BldJpwpQvvVi6ygxdzN +erJnB15G7xhUkGzDI2xhIJw3e6NGqf1PMB4CTNna6eN2cKYAxPfsWo5Pyh1YtU4s +5h+B3+43ol32ccBiRo4YXagbYMELjspEf0AvObvMWSxZQoBHcJ5JGEApxcgvFu8i +FBSALVy1IrYE3gXAv8TB0AK7IpuNIL48v5JXCA6JOGYbXFljj6aLFTzfrV3lzhQ0 +kqBVnQNqVfOUQNUhNT93bnEWVf911j/af5zuFtmr1kbMzucCAwEAAaN2MHQwHQYD +VR0OBBYEFHZOtGQHV3roaj3nlQ1XRU0O+05TMB8GA1UdIwQYMBaAFHZOtGQHV3ro +aj3nlQ1XRU0O+05TMA8GA1UdEwEB/wQFMAMBAf8wIQYDVR0RBBowGIcEwAUIEIcQ +/oAAAAAAAAA8KS+h3UQHZTANBgkqhkiG9w0BAQsFAAOCAQEAcvqvtUSJ2JM3rrWj +XjCOhosKY/cow4oDAVdn8AvI/Z4FJfcQZ1vA+ZM533/TaJStG4ThfjyX9t1Ej08M +UzP4ZUyXJTv8o6C6j5e9ggEwo/cFp1iWP+xr2SXLJ2cabnu8db5FN5J75HjNsuVs +PM95LYY9VlTm9W7JxMwkPEIG+wH5zu6Hj45UAAamwwjOKT1hJYumxdmLAp1oyG1p +u86b8iVUjiHG+K6qr4hAKXhuSXE1s/pYqcn1feyk2SbkKvGFR6ad+gmdT4ZaiNYT +nL8+t2wim/fRkV0CNdWrrJpWtLzjPq1al7g2eIopdLufSlqanouVpnzwuKGN5QC/ +MuDohA== +-----END CERTIFICATE----- diff --git a/tests/auto/network/ssl/qsslsocket/tst_qsslsocket.cpp b/tests/auto/network/ssl/qsslsocket/tst_qsslsocket.cpp index ca6029685d..05356eacfc 100644 --- a/tests/auto/network/ssl/qsslsocket/tst_qsslsocket.cpp +++ b/tests/auto/network/ssl/qsslsocket/tst_qsslsocket.cpp @@ -1710,6 +1710,17 @@ void tst_QSslSocket::isMatchingHostname() QCOMPARE(QSslSocketPrivate::isMatchingHostname(cert, QString::fromUtf8("foo.foo.xn--schufele-2za.de")), false); QCOMPARE(QSslSocketPrivate::isMatchingHostname(cert, QString::fromUtf8("www.schaufele.de")), false); QCOMPARE(QSslSocketPrivate::isMatchingHostname(cert, QString::fromUtf8("www.schufele.de")), false); + + /* Generated with the following command (only valid with openssl >= 1.1.1 due to "-addext"): + openssl req -x509 -nodes -subj "/CN=example.org" \ + -addext "subjectAltName = IP:192.5.8.16, IP:fe80::3c29:2fa1:dd44:765" \ + -newkey rsa:2048 -keyout /dev/null -out subjectAltNameIP.crt + */ + certs = QSslCertificate::fromPath(testDataDir + "certs/subjectAltNameIP.crt"); + QVERIFY(!certs.isEmpty()); + cert = certs.first(); + QCOMPARE(QSslSocketPrivate::isMatchingHostname(cert, QString::fromUtf8("192.5.8.16")), true); + QCOMPARE(QSslSocketPrivate::isMatchingHostname(cert, QString::fromUtf8("fe80::3c29:2fa1:dd44:765")), true); } void tst_QSslSocket::wildcard() -- cgit v1.2.3