QSslCertificate: overhaul ASN.1 datetime parsing

Instead of manual string splitting (EW!), use QDateTime parsing.
Moreover, X.509 certificates *must* have a valid start/end date.
In case of parsing failure, reject the certificate. An autotest
for this last case is coming in a separate patch.

Change-Id: I934bf9e6a4a92e4befdb3b0f9450f76f67bad067
Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
This commit is contained in:
Giuseppe D'Angelo 2020-03-24 01:14:52 +01:00
parent 7a1650e343
commit ed2c3f6756
3 changed files with 66 additions and 45 deletions

View File

@ -46,7 +46,6 @@
#include <QDebug>
#include <limits>
#include <locale>
QT_BEGIN_NAMESPACE
@ -85,27 +84,6 @@ static OidNameMap createOidMap()
}
Q_GLOBAL_STATIC_WITH_ARGS(OidNameMap, oidNameMap, (createOidMap()))
static bool stringToNonNegativeInt(const QByteArray &asnString, int *val)
{
// Helper function for toDateTime(), which handles chunking of the original
// string into smaller sub-components, so we expect the whole 'asnString' to
// be a valid non-negative number.
Q_ASSERT(val);
// We want the C locale, as used by QByteArray; however, no leading sign is
// allowed (which QByteArray would accept), so we have to check the data:
const std::locale localeC;
for (char v : asnString) {
if (!std::isdigit(v, localeC))
return false;
}
bool ok = false;
*val = asnString.toInt(&ok);
Q_ASSERT(ok && *val >= 0);
return true;
}
QAsn1Element::QAsn1Element(quint8 type, const QByteArray &value)
: mType(type)
, mValue(value)
@ -256,30 +234,61 @@ bool QAsn1Element::toBool(bool *ok) const
QDateTime QAsn1Element::toDateTime() const
{
if (mValue.endsWith('Z')) {
if (mType == UtcTimeType && mValue.size() == 13) {
int year = 0;
if (!stringToNonNegativeInt(mValue.mid(0, 2), &year))
return QDateTime();
// RFC 2459: YY represents a year in the range [1950, 2049]
return QDateTime(QDate(year < 50 ? 2000 + year : 1900 + year,
mValue.mid(2, 2).toInt(),
mValue.mid(4, 2).toInt()),
QTime(mValue.mid(6, 2).toInt(),
mValue.mid(8, 2).toInt(),
mValue.mid(10, 2).toInt()),
Qt::UTC);
} else if (mType == GeneralizedTimeType && mValue.size() == 15) {
return QDateTime(QDate(mValue.mid(0, 4).toInt(),
mValue.mid(4, 2).toInt(),
mValue.mid(6, 2).toInt()),
QTime(mValue.mid(8, 2).toInt(),
mValue.mid(10, 2).toInt(),
mValue.mid(12, 2).toInt()),
Qt::UTC);
}
QDateTime result;
if (mValue.size() != 13 && mValue.size() != 15)
return result;
// QDateTime::fromString is lenient and accepts +- signs in front
// of the year; but ASN.1 doesn't allow them.
const auto isAsciiDigit = [](QChar c)
{
return c >= QLatin1Char('0') && c <= QLatin1Char('9');
};
if (!isAsciiDigit(mValue[0]))
return result;
// Timezone must be present, and UTC
if (mValue.back() != 'Z')
return result;
// In addition, check that we only have digits representing the
// date/time. This should not really be necessary (there's no such
// thing as negative months/days/etc.); it's a workaround for
// QTBUG-84349.
if (!std::all_of(mValue.begin(), mValue.end() - 1, isAsciiDigit))
return result;
if (mType == UtcTimeType && mValue.size() == 13) {
result = QDateTime::fromString(QString::fromLatin1(mValue),
QStringLiteral("yyMMddHHmmsst"));
if (!result.isValid())
return result;
Q_ASSERT(result.timeSpec() == Qt::UTC);
QDate date = result.date();
// RFC 2459:
// Where YY is greater than or equal to 50, the year shall be
// interpreted as 19YY; and
//
// Where YY is less than 50, the year shall be interpreted as 20YY.
//
// QDateTime interprets the 'yy' format as 19yy, so we may need to adjust
// the year (bring it in the [1950, 2049] range).
if (date.year() < 1950)
result.setDate(date.addYears(100));
Q_ASSERT(result.date().year() >= 1950);
Q_ASSERT(result.date().year() <= 2049);
} else if (mType == GeneralizedTimeType && mValue.size() == 15) {
result = QDateTime::fromString(QString::fromLatin1(mValue),
QStringLiteral("yyyyMMddHHmmsst"));
}
return QDateTime();
return result;
}
QMultiMap<QByteArray, QString> QAsn1Element::toInfo() const

View File

@ -346,10 +346,16 @@ bool QSslCertificatePrivate::parse(const QByteArray &data)
return false;
notValidBefore = elem.toDateTime();
if (!notValidBefore.isValid())
return false;
if (!elem.read(validityStream) || (elem.type() != QAsn1Element::UtcTimeType && elem.type() != QAsn1Element::GeneralizedTimeType))
return false;
notValidAfter = elem.toDateTime();
if (!notValidAfter.isValid())
return false;
// subject name
if (!elem.read(certStream) || elem.type() != QAsn1Element::SequenceType)

View File

@ -170,6 +170,12 @@ void tst_QAsn1Element::dateTime_data()
QTest::newRow("GeneralizedTime - no trailing Z")
<< QByteArray::fromHex("180f323035313038323930393533343159")
<< QDateTime();
QTest::newRow("GeneralizedTime - invalid month (+8)")
<< QByteArray::fromHex("180f323035312b3832393039353334315a")
<< QDateTime();
QTest::newRow("GeneralizedTime - invalid date (+9)")
<< QByteArray::fromHex("180f3230353130382b393039353334315a")
<< QDateTime();
}
void tst_QAsn1Element::dateTime()