diff --git a/CMakeLists.txt b/CMakeLists.txt index 30f5a2f..2717353 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,129 +1,128 @@ cmake_minimum_required(VERSION 3.5) set(PIM_VERSION "5.13.40") project(KItinerary VERSION ${PIM_VERSION}) set(CMAKE_CXX_STANDARD 14) set(KF5_MIN_VERSION "5.64.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMAddTests) include(ECMGenerateHeaders) include(ECMQtDeclareLoggingCategory) include(ECMSetupVersion) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(GenerateExportHeader) ecm_setup_version(PROJECT VARIABLE_PREFIX KITINERARY VERSION_HEADER kitinerary_version.h PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KPimItineraryConfigVersion.cmake" ) set(QT_REQUIRED_VERSION "5.12.0") find_package(Qt5 ${QT_REQUIRED_VERSION} REQUIRED COMPONENTS Gui Qml) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS I18n) if (NOT ANDROID) find_package(SharedMimeInfo 1.3 REQUIRED) endif() set(KMIME_VERSION "5.13.40") set(PIM_PKPASS "5.13.40") find_package(KF5Mime ${KMIME_VERSION} CONFIG REQUIRED) find_package(KF5CalendarCore ${KF5_MIN_VERSION} CONFIG) find_package(KF5Contacts ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KPimPkPass ${PIM_PKPASS} CONFIG REQUIRED) find_package(Poppler COMPONENTS Core) set_package_properties("Poppler" PROPERTIES TYPE OPTIONAL PURPOSE "Support for extraction from PDF booking confirmations.") find_package(ZXing CONFIG) set_package_properties("ZXing" PROPERTIES TYPE OPTIONAL PURPOSE "Support for barcode decoding." URL "https://github.com/nu-book/zxing-cpp") find_package(ZLIB REQUIRED) set_package_properties("ZLIB" PROPERTIES PURPOSE "Support for decoding UIC 918-3 train tickets.") find_package(LibXml2 MODULE) set_package_properties("LibXml2" PROPERTIES PURPOSE "Support for extraction from HTML booking confirmations." URL "http://libxml.org") find_package(PhoneNumber OPTIONAL_COMPONENTS PhoneNumber QUIET) set_package_properties("PhoneNumber" PROPERTIES PURPOSE "Parsing and geo-coding of phone numbers.") find_package(OpenSSL 1.1) set_package_properties("OpenSSL" PROPERTIES TYPE OPTIONAL PURPOSE "VDV ticket decoding." URL "https://openssl.org") if (NOT ANDROID) set_package_properties(KF5CalendarCore PROPERTIES TYPE REQUIRED) set_package_properties(LibXml2 PROPERTIES TYPE REQUIRED) endif() if(TARGET Poppler::Core) # check if we have private Poppler headers find_file(HAVE_POPPLER_UNSTABLE_HEADERS "OutputDev.h" PATHS ${Poppler_INCLUDE_DIRS} NO_DEFAULT_PATH) if (NOT HAVE_POPPLER_UNSTABLE_HEADERS) message(WARNING "Poppler was not build with ENABLE_UNSTABLE_API_ABI_HEADER - building without PDF support!") set(HAVE_POPPLER FALSE) else() set(HAVE_POPPLER TRUE) endif() endif() if (HAVE_POPPLER) string(REPLACE "." ";" _poppler_version_components ${Poppler_VERSION}) list(GET _poppler_version_components 0 POPPLER_VERSION_MAJOR) list(GET _poppler_version_components 1 POPPLER_VERSION_MINOR) list(GET _poppler_version_components 2 POPPLER_VERSION_PATCH) endif() if (TARGET ZXing::Core) set(HAVE_ZXING ON) endif() if (TARGET KF5::CalendarCore) set(HAVE_KCAL ON) endif() if (LIBXML2_FOUND) set(HAVE_LIBXML2 ON) endif() if (TARGET PhoneNumber::PhoneNumber) set(HAVE_PHONENUMBER ON) endif() if (TARGET OpenSSL::Crypto) - # TODO breaks FreeBSD build due to not finding the ssl/rsa.h include - #set(HAVE_OPENSSL_RSA ON) + set(HAVE_OPENSSL_RSA ON) endif() add_definitions(-DTRANSLATION_DOMAIN=\"kitinerary\") if (EXISTS "${CMAKE_SOURCE_DIR}/.git") add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x060000) endif() add_definitions(-DQT_NO_FOREACH) add_subdirectory(src) if(BUILD_TESTING) add_subdirectory(autotests) endif() feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) set(CMAKECONFIG_INSTALL_DIR "${CMAKECONFIG_INSTALL_PREFIX}/KPimItinerary") configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/KPimItineraryConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KPimItineraryConfig.cmake" INSTALL_DESTINATION "${CMAKECONFIG_INSTALL_DIR}" ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KPimItineraryConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KPimItineraryConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel) install(EXPORT KPimItineraryTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KPimItineraryTargets.cmake NAMESPACE KPim:: ) install(FILES org_kde_kitinerary.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) diff --git a/src/vdv/certs/4445564456110116.vdv-cert b/src/vdv/certs/4445564456110116.vdv-cert new file mode 100644 index 0000000..94e6be6 Binary files /dev/null and b/src/vdv/certs/4445564456110116.vdv-cert differ diff --git a/src/vdv/certs/4445564456110216.vdv-cert b/src/vdv/certs/4445564456110216.vdv-cert new file mode 100644 index 0000000..d85b44e Binary files /dev/null and b/src/vdv/certs/4445564456110216.vdv-cert differ diff --git a/src/vdv/certs/8.vdv-cert b/src/vdv/certs/4445564456110506.vdv-cert similarity index 100% rename from src/vdv/certs/8.vdv-cert rename to src/vdv/certs/4445564456110506.vdv-cert diff --git a/src/vdv/certs/2.vdv-cert b/src/vdv/certs/4445564456110706.vdv-cert similarity index 100% rename from src/vdv/certs/2.vdv-cert rename to src/vdv/certs/4445564456110706.vdv-cert diff --git a/src/vdv/certs/4445564456110811.vdv-cert b/src/vdv/certs/4445564456110811.vdv-cert new file mode 100644 index 0000000..961890c Binary files /dev/null and b/src/vdv/certs/4445564456110811.vdv-cert differ diff --git a/src/vdv/certs/4445564456110816.vdv-cert b/src/vdv/certs/4445564456110816.vdv-cert new file mode 100644 index 0000000..c46a2eb Binary files /dev/null and b/src/vdv/certs/4445564456110816.vdv-cert differ diff --git a/src/vdv/certs/4555564456100106.vdv-cert b/src/vdv/certs/4555564456100106.vdv-cert new file mode 100644 index 0000000..7d87583 Binary files /dev/null and b/src/vdv/certs/4555564456100106.vdv-cert differ diff --git a/src/vdv/certs/NOTES b/src/vdv/certs/NOTES index 9801a33..9c6ebe8 100644 --- a/src/vdv/certs/NOTES +++ b/src/vdv/certs/NOTES @@ -1,5 +1,4 @@ -The two CA certs in here are manually retrieved from: -ldap://ldap-vdv-ion.telesec.de:389/cn=4445564456110506,ou=VDV%20KA,o=VDV%20Kernapplikations%20GmbH,c=de -ldap://ldap-vdv-ion.telesec.de:389/cn=4445564456110706,ou=VDV%20KA,o=VDV%20Kernapplikations%20GmbH,c=de +The CA certs in here are manually retrieved from: +ldap://ldap-vdv-ion.telesec.de:389/cn=,ou=VDV%20KA,o=VDV%20Kernapplikations%20GmbH,c=de TODO: do this automatically for all relevant certs diff --git a/src/vdv/certs/vdv-certs.qrc b/src/vdv/certs/vdv-certs.qrc index 4d9c067..58bdf7e 100644 --- a/src/vdv/certs/vdv-certs.qrc +++ b/src/vdv/certs/vdv-certs.qrc @@ -1,6 +1,10 @@ - 2.vdv-cert - 8.vdv-cert + 4445564456110216.vdv-cert + 4445564456110506.vdv-cert + 4445564456110706.vdv-cert + 4445564456110811.vdv-cert + 4445564456110816.vdv-cert + 4555564456100106.vdv-cert diff --git a/src/vdv/iso9796_2decoder.cpp b/src/vdv/iso9796_2decoder.cpp index 1446cc6..9c7e047 100644 --- a/src/vdv/iso9796_2decoder.cpp +++ b/src/vdv/iso9796_2decoder.cpp @@ -1,78 +1,87 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "iso9796_2decoder_p.h" #include #ifdef HAVE_OPENSSL_RSA -#include +#include +#include #endif using namespace KItinerary; Iso9796_2Decoder::Iso9796_2Decoder() #ifdef HAVE_OPENSSL_RSA : m_rsa(RSA_new(), RSA_free) #endif { } Iso9796_2Decoder::~Iso9796_2Decoder() = default; void Iso9796_2Decoder::setRsaParameters(const uint8_t *modulus, uint16_t modulusSize, const uint8_t *exponent, uint16_t exponentSize) { #ifdef HAVE_OPENSSL_RSA const auto n = BN_bin2bn(modulus, modulusSize, nullptr); const auto e = BN_bin2bn(exponent, exponentSize, nullptr); RSA_set0_key(m_rsa.get(), n, e, nullptr); // takes ownership of n and e #else Q_UNUSED(modulus); Q_UNUSED(modulusSize); Q_UNUSED(exponent); Q_UNUSED(exponentSize); #endif } void Iso9796_2Decoder::addWithRecoveredMessage(const uint8_t *data, int size) { #ifdef HAVE_OPENSSL_RSA QByteArray out; out.resize(RSA_size(m_rsa.get())); const auto outSize = RSA_public_decrypt(size, data, (uint8_t*)out.data(), m_rsa.get(), RSA_NO_PADDING); + if (outSize < 0) { + qWarning() << "RSA error:" << ERR_error_string(ERR_get_error(), nullptr); + return; + } + out.resize(outSize); - qDebug() << outSize << out.toHex(); + if ((uint8_t)out[0] != 0x6a || (uint8_t)out[out.size() - 1] != 0xbc || out.size() < 22) { // 20 byte SHA-1 + padding/trailer + qWarning() << "RSA message recovery failed:" << out.toHex() << outSize; + return; + } + + m_recoveredMsg.append(out.constData() + 1, out.size() - 22); #else Q_UNUSED(data); Q_UNUSED(size); #endif } void Iso9796_2Decoder::add(const uint8_t *data, int size) { -#ifdef HAVE_OPENSSL_RSA - // TODO -#else - Q_UNUSED(data); - Q_UNUSED(size); -#endif + if (m_recoveredMsg.isEmpty()) { // previous failure + return; + } + m_recoveredMsg.append((const char*)data, size); } QByteArray Iso9796_2Decoder::recoveredMessage() const { - return {}; // TODO + return m_recoveredMsg; } diff --git a/src/vdv/iso9796_2decoder_p.h b/src/vdv/iso9796_2decoder_p.h index 9aa93e7..be3cbfc 100644 --- a/src/vdv/iso9796_2decoder_p.h +++ b/src/vdv/iso9796_2decoder_p.h @@ -1,60 +1,61 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KITINERARY_ISO9796_2DECODER_P_H #define KITINERARY_ISO9796_2DECODER_P_H #include "config-kitinerary.h" #include #ifdef HAVE_OPENSSL_RSA -#include +#include #endif #include #include namespace KItinerary { /** Message recovery for ISO 9796-2 Schema 1 signatures. * This does not care at all about security or actually validating the signature, * this is merely about recoverying the part of the signed message that is mangled * by the signature. */ class Iso9796_2Decoder { public: Iso9796_2Decoder(); ~Iso9796_2Decoder(); void setRsaParameters(const uint8_t *modulus, uint16_t modulusSize, const uint8_t *exponent, uint16_t exponentSize); void addWithRecoveredMessage(const uint8_t *data, int size); void add(const uint8_t *data, int size); QByteArray recoveredMessage() const; private: #ifdef HAVE_OPENSSL_RSA std::unique_ptr m_rsa; #endif + QByteArray m_recoveredMsg; }; } #endif // KITINERARY_ISO9796_2DECODER_P_H diff --git a/src/vdv/vdvcertificate.cpp b/src/vdv/vdvcertificate.cpp index a63c126..8afe810 100644 --- a/src/vdv/vdvcertificate.cpp +++ b/src/vdv/vdvcertificate.cpp @@ -1,113 +1,198 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "vdvcertificate_p.h" #include "vdvdata_p.h" +#include "iso9796_2decoder_p.h" #include #include using namespace KItinerary; VdvCertificate::VdvCertificate() = default; VdvCertificate::VdvCertificate(const QByteArray &data, int offset) : m_offset(offset) { if ((unsigned)data.size() <= m_offset + sizeof(VdvCertificateHeader)) { qWarning() << "Certificate data too small:" << data.size() << offset; return; } m_data = data; const auto hdr = header(); if (!hdr->isValid() || data.size() < hdr->size() + offset) { qWarning() << "Invalid certificate header:" << hdr->isValid() << hdr->size() << data.size() << offset; m_data.clear(); return; } const auto certKeyBlock = hdr->contentAt(0); - if (!certKeyBlock->isValid()) { - qWarning() << "Invalid certificate key block."; + if (certKeyBlock->isValid()) { + m_type = Raw; + qDebug() << "found decrypted key"; + qDebug() << "car:" << QByteArray(certKey()->car.region, 2) << QByteArray(certKey()->car.name, 3); + qDebug() << "chr:" << QByteArray(certKey()->chr.name, 5) << certKey()->chr.algorithmReference << certKey()->chr.year; + qDebug() << "cha:" << QByteArray(certKey()->cha.name, 6); + qDebug() << "modulus:" << modulusSize() << *modulus() << *(modulus() + modulusSize() - 1) << (modulus() - (const uint8_t*)certKey()); + qDebug() << "exponent:" << exponentSize() << *exponent() << *(exponent() + exponentSize() - 1) << (exponent() - (const uint8_t*)certKey()); + return; + } + + const auto sig = hdr->contentAt(0); + if (!sig->isValid()) { + qWarning() << "Invalid certificate content: neither a key nor a signature!"; m_data.clear(); return; } - qDebug() << "car:" << QByteArray(certKey()->car.region, 2) << QByteArray(certKey()->car.name, 3); - qDebug() << "chr:" << QByteArray(certKey()->chr.name, 5) << certKey()->chr.algorithmReference << certKey()->chr.year; - qDebug() << "cha:" << QByteArray(certKey()->cha.name, 6); - qDebug() << "modulus:" << modulusSize() << *modulus() << *(modulus() + modulusSize() - 1); - qDebug() << "exponent:" << exponentSize() << *exponent() << *(exponent() + exponentSize() - 1); + m_type = Signed; + qDebug() << "found encrypted key"; } VdvCertificate::~VdvCertificate() = default; bool VdvCertificate::isValid() const { - return !m_data.isEmpty(); + if (m_type == Invalid) { + return false; + } + return m_type == Signed ? !m_recoveredData.isEmpty() : !m_data.isEmpty(); +} + +bool VdvCertificate::needsCaKey() const +{ + return m_type == Signed && m_recoveredData.isEmpty(); +} + +int VdvCertificate::size() const +{ + return m_type == Invalid ? 0 : header()->size(); } uint16_t VdvCertificate::modulusSize() const { switch (certKey()->certificateProfileIdentifier) { case 3: return 1536 / 8; case 4: return 1024 / 8; + case 7: + return 1984 / 8; } qWarning() << "Unknown certificate profile identifier: " << certKey()->certificateProfileIdentifier; return 0; } const uint8_t* VdvCertificate::modulus() const { - return &(certKey()->modulusBegin); + const auto k = certKey(); + return (&k->oidBegin) + k->oidSize(); } uint16_t VdvCertificate::exponentSize() const { return 4; } const uint8_t* VdvCertificate::exponent() const { - return &(certKey()->modulusBegin) + modulusSize(); + return modulus() + modulusSize(); +} + +void VdvCertificate::setCaCertificate(const VdvCertificate &caCert) +{ + if (!caCert.isValid()) { + qWarning() << "Invalid CA certificate."; + return; + } + + Iso9796_2Decoder decoder; + decoder.setRsaParameters(caCert.modulus(), caCert.modulusSize(), caCert.exponent(), caCert.exponentSize()); + + const auto sig = header()->contentAt(0); + decoder.addWithRecoveredMessage(sig->contentData(), sig->contentSize()); + + if (header()->contentSize() > sig->size()) { + const auto rem = header()->contentAt(sig->size()); + if (rem->isValid() && rem->size() + sig->size() >= header()->contentSize()) { + decoder.add(rem->contentData(), rem->contentSize()); + } else { + qWarning() << "Invalid signature remainder!" << rem->isValid() << rem->size() << sig->size() << header()->contentSize(); + } + qDebug() << rem->isValid() << rem->contentOffset() << rem->contentSize(); + } + + m_recoveredData = decoder.recoveredMessage(); + qDebug() << m_recoveredData.toHex() << m_recoveredData.size(); + if (!m_recoveredData.isEmpty() && m_recoveredData.size() >= (certKey()->headerSize() + modulusSize() + exponentSize())) { + qDebug() << "successfully decrypted key"; + qDebug() << "car:" << QByteArray(certKey()->car.region, 2) << QByteArray(certKey()->car.name, 3); + qDebug() << "chr:" << QByteArray(certKey()->chr.name, 5) << certKey()->chr.algorithmReference << certKey()->chr.year; + qDebug() << "cha:" << QByteArray(certKey()->cha.name, 6); + qDebug() << "modulus:" << modulusSize() << *modulus() << *(modulus() + modulusSize() - 1) << (modulus() - (const uint8_t*)certKey()); + qDebug() << "exponent:" << exponentSize() << *exponent() << *(exponent() + exponentSize() - 1) << (exponent() - (const uint8_t*)certKey()); + } else { + qWarning() << "decrypting certificate key failed!"; + qDebug() << "size is:" << m_recoveredData.size() << "expected:" << (certKey()->headerSize() + modulusSize() + exponentSize()); + qDebug() << QByteArray((const char*)caCert.modulus(), caCert.modulusSize()).toHex(); + qDebug() << QByteArray((const char*)caCert.exponent(), caCert.exponentSize()).toHex(); + qDebug() << QByteArray((const char*)sig->contentData(), sig->contentSize()).toHex();; + m_type = Invalid; + m_recoveredData.clear(); + } } const VdvCertificateHeader* VdvCertificate::header() const { return reinterpret_cast(m_data.constData() + m_offset); } const VdvCertificateKey* VdvCertificate::certKey() const { - // TODO check if m_data is large enough - return header()->contentAt(0)->contentAt(0); + if (m_type == Signed) { + return reinterpret_cast(m_recoveredData.constData()); + } else if (m_type == Raw) { + return header()->contentAt(0)->contentAt(0); + } + return nullptr; } -VdvCertificate VdvPkiRepository::caCertificate(uint8_t serNum) +VdvCertificate VdvPkiRepository::caCertificate(const VdvCaReference *car) { - QFile f(QLatin1String(":/org.kde.pim/kitinerary/vdv/certs/") + QString::number(serNum) + QLatin1String(".vdv-cert")); + QFile f(QLatin1String(":/org.kde.pim/kitinerary/vdv/certs/") + + QString::fromLatin1(QByteArray(reinterpret_cast(car), sizeof(VdvCaReference)).toHex()) + + QLatin1String(".vdv-cert")); if (!f.open(QFile::ReadOnly)) { - qWarning() << "Failed to open CA cert file" << serNum << f.errorString(); + qWarning() << "Failed to open CA cert file" << f.fileName() << f.errorString(); return VdvCertificate(); } - qDebug() << f.size(); - return VdvCertificate(f.readAll()); + VdvCertificate cert(f.readAll()); + if (cert.needsCaKey()) { + VdvCaReference rootCAR; + rootCAR.region[0] = 'E'; rootCAR.region[1] = 'U'; + rootCAR.name[0] = 'V'; rootCAR.name[1] = 'D'; rootCAR.name[2] = 'V'; + rootCAR.serviceIndicator = 0; + rootCAR.discretionaryData = 1; + rootCAR.algorithmReference = 1; + rootCAR.year = 6; + cert.setCaCertificate(caCertificate(&rootCAR)); + } + return cert; } diff --git a/src/vdv/vdvcertificate_p.h b/src/vdv/vdvcertificate_p.h index f9b3e82..8279ffc 100644 --- a/src/vdv/vdvcertificate_p.h +++ b/src/vdv/vdvcertificate_p.h @@ -1,65 +1,84 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KITINERARY_VDVCERTIFICATE_H #define KITINERARY_VDVCERTIFICATE_H #include namespace KItinerary { +struct VdvCaReference; struct VdvCertificateHeader; struct VdvCertificateKey; -/** Certificate object, to obtain the RSA parameters. */ +/** Certificate object, to obtain the RSA parameters. + * This can be both a raw certificate which can be directly consumed, + * or one with an ISO 9796-2 signature with message recovery. In the latter + * case you need to provide the key of the corresponding CA certificate + * for decoding too. +*/ class VdvCertificate { public: VdvCertificate(); explicit VdvCertificate(const QByteArray &data, int offset = 0); ~VdvCertificate(); bool isValid() const; + bool needsCaKey() const; + + /** Size of the entire encoded certificate data. */ + int size() const; /** Amount of bytes in the RSA modulus. */ uint16_t modulusSize() const; /** RSA modulus. */ const uint8_t* modulus() const; /** Amount of bytes in the RSA exponent. */ uint16_t exponentSize() const; /** RSA exponent. */ const uint8_t* exponent() const; + /** Sets the CA certificate for decoding ISO 9796-2 signed certificates. */ + void setCaCertificate(const VdvCertificate &caCert); + private: const VdvCertificateHeader *header() const; const VdvCertificateKey *certKey() const; QByteArray m_data; + QByteArray m_recoveredData; int m_offset = 0; + enum CertificateType { + Invalid, + Raw, + Signed + } m_type = Invalid; }; /** VDV (sub)CA certificate access. */ namespace VdvPkiRepository { - /** Returns the (sub)CA certificate for the given serial number. */ - VdvCertificate caCertificate(uint8_t serNum); + /** Returns the (sub)CA certificate for the given CA Reference (CAR). */ + VdvCertificate caCertificate(const VdvCaReference *car); } } #endif // KITINERARY_VDVCERTIFICATE_H diff --git a/src/vdv/vdvdata_p.h b/src/vdv/vdvdata_p.h index 4dc6266..7974171 100644 --- a/src/vdv/vdvdata_p.h +++ b/src/vdv/vdvdata_p.h @@ -1,195 +1,204 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KITINERARY_VDVDATA_P_H #define KITINERARY_VDVDATA_P_H #include #include namespace KItinerary { enum : uint8_t { TagSignature = 0x9E, TagSignatureRemainder = 0x9A, TagCaReference = 0x42, TagOneByteSize = 0x81, TagTwoByteSize = 0x82, }; enum : uint16_t { TagCertificate = 0x7F21, TagCertificateSignature = 0x5F37, TagCertificateSignatureRemainder = 0x5F38, TagCertificateContent = 0x5F4E, }; #pragma pack(push) #pragma pack(1) /** Generic structure for the header of data blocks in VDV binary data. * This consits of: * - a one or two byte tag (@tparam TagType) with a fixed value (@tparam TagValue) * - a one byte field indicating the size of the size field (optional) * - one or two bytes for the size * - followed by size bytes of content */ template struct VdvAbstractDataBlock { TagType tag; inline bool isValid() const { return qFromBigEndian(tag) == TagValue; } }; template struct VdvSimpleDataBlock : public VdvAbstractDataBlock { uint8_t size0; inline uint16_t contentSize() const { return size0; } inline uint16_t contentOffset() const { return sizeof(VdvSimpleDataBlock); } inline const uint8_t* contentData() const { return reinterpret_cast(this) + contentOffset(); } inline uint16_t size() const { return contentSize() + contentOffset(); } template inline const T* contentAt(int offset) const { return reinterpret_cast(contentData() + offset); } }; template struct VdvTaggedSizeDataBlock : public VdvAbstractDataBlock { uint8_t sizeTag; uint8_t size0; uint8_t size1; inline bool isValid() const { return VdvAbstractDataBlock::isValid() && (sizeTag == TagOneByteSize || sizeTag == TagTwoByteSize); } inline uint16_t contentSize() const { return sizeTag == TagOneByteSize ? size0 : ((size0 << 8) + size1); } inline uint16_t contentOffset() const { return sizeof(VdvTaggedSizeDataBlock) - ((sizeTag == TagOneByteSize) ? 1 : 0); } inline const uint8_t* contentData() const { return reinterpret_cast(this) + contentOffset(); } inline uint16_t size() const { return contentSize() + contentOffset(); } template inline const T* contentAt(int offset) const { return reinterpret_cast(contentData() + offset); } }; /** Signature container for the signed part of the payload data. */ struct VdvSignature : public VdvTaggedSizeDataBlock {}; /** Signature Remainder header. */ struct VdvSignatureRemainder : public VdvSimpleDataBlock { enum { Offset = 131 }; }; /** CV certificate. */ struct VdvCertificateHeader : public VdvTaggedSizeDataBlock { }; /** Certificate Authority Reference (CAR) content. */ struct VdvCaReference { char region[2]; char name[3]; uint8_t serviceIndicator: 4; uint8_t discretionaryData: 4; uint8_t algorithmReference; uint8_t year; }; struct VdvCaReferenceBlock : public VdvSimpleDataBlock {}; /** Certificate Holder Reference (CHR) */ struct VdvCertificateHolderReference { uint8_t filler[4]; // always null char name[5]; uint8_t serviceIndicator: 4; uint8_t discretionaryData: 4; uint8_t algorithmReference; uint8_t year; }; /** Certificate Holder Authorization (CHA) */ struct VdvCertificateHolderAuthorization { char name[6]; uint8_t stuff; }; /** Certificate key, contained in a certificate object. */ struct VdvCertificateKey { uint8_t certificateProfileIdentifier; VdvCaReference car; VdvCertificateHolderReference chr; VdvCertificateHolderAuthorization cha; uint8_t date[4]; - uint8_t oid[9]; - uint8_t modulusBegin; + uint8_t oidBegin; + + inline uint8_t oidSize() const + { + return oidBegin == 0x2a ? 9 : 7; // ugly, but works for now + } + + inline uint8_t headerSize() const + { + return sizeof(VdvCertificateKey) + oidSize() - 1; + } }; struct VdvCertificateKeyBlock : public VdvTaggedSizeDataBlock {}; /** Certificate signature. */ struct VdvCertificateSignature : public VdvTaggedSizeDataBlock {}; /** Certificate signature remainder. */ struct VdvCertificateSignatureRemainder : public VdvSimpleDataBlock {}; #pragma pack(pop) } #endif // KITINERARY_VDVDATA_P_H diff --git a/src/vdv/vdvticketparser.cpp b/src/vdv/vdvticketparser.cpp index dcd1a17..6d39c88 100644 --- a/src/vdv/vdvticketparser.cpp +++ b/src/vdv/vdvticketparser.cpp @@ -1,119 +1,109 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "vdvticketparser.h" #include "vdvdata_p.h" #include "vdvcertificate_p.h" #include "iso9796_2decoder_p.h" #include #include using namespace KItinerary; VdvTicketParser::VdvTicketParser() = default; VdvTicketParser::~VdvTicketParser() = default; void VdvTicketParser::parse(const QByteArray &data) { qDebug() << data.size(); if (!maybeVdvTicket(data)) { qWarning() << "Input data is not a VDV ticket!"; return; } // (1) find the certificate authority reference (CAR) to identify the key to decode the CV certificate const auto sigRemainder = reinterpret_cast(data.constData() + VdvSignatureRemainder::Offset); if (!sigRemainder->isValid() || VdvSignatureRemainder::Offset + sigRemainder->size() + sizeof(VdvCertificateHeader) > (unsigned)data.size()) { qWarning() << "Invalid VDV signature remainder."; return; } qDebug() << sigRemainder->contentSize(); const auto cvCertOffset = VdvSignatureRemainder::Offset + sigRemainder->size(); - const auto cvCert = reinterpret_cast(data.constData() + cvCertOffset); - if (!cvCert->isValid() || cvCertOffset + cvCert->size() + sizeof(VdvCaReferenceBlock) > (unsigned)data.size()) { - qWarning() << "Invalid CV signature:" << cvCert->isValid() << cvCertOffset << cvCert->size(); + auto cvCert = VdvCertificate(data ,cvCertOffset); + if ((!cvCert.isValid() && !cvCert.needsCaKey()) || cvCertOffset + cvCert.size() + sizeof(VdvCaReferenceBlock) > (unsigned)data.size()) { + qWarning() << "Invalid CV signature:" << cvCert.isValid() << cvCertOffset << cvCert.size(); return; } - qDebug() << cvCert->contentSize(); - const auto carOffset = cvCertOffset + cvCert->size(); + const auto carOffset = cvCertOffset + cvCert.size(); const auto carBlock = reinterpret_cast(data.constData() + carOffset); if (!carBlock->isValid() || carBlock->contentSize() < sizeof(VdvCaReference)) { qWarning() << "Invalid CA Reference."; return; } const auto car = carBlock->contentAt(0); qDebug() << QByteArray(car->name, 3) << car->serviceIndicator << car->discretionaryData << car->algorithmReference << car->year; - const auto caCert = VdvPkiRepository::caCertificate(car->algorithmReference); + const auto caCert = VdvPkiRepository::caCertificate(car); if (!caCert.isValid()) { - qWarning() << "Could not find CA certificate" << car->algorithmReference; + qWarning() << "Could not find CA certificate" << QByteArray(reinterpret_cast(car), sizeof(VdvCaReference)).toHex(); return; } // (2) decode the CV certificate - const auto cvSig = cvCert->contentAt(0); - if (!cvSig->isValid()) { - qWarning() << "Invalid CV certificate signature structure."; - return; - } - qDebug() << cvCert->contentSize() << cvSig->size() << (uint8_t)*(cvCert->contentData() + cvSig->size()); - const auto cvRem = cvCert->contentAt(cvSig->size()); - if (!cvRem->isValid()) { - qWarning() << "Invalid CV certificate signature remainder structure."; - return; - } - qDebug() << cvSig->contentSize() << cvRem->contentSize(); - - Iso9796_2Decoder cvDecoder; - cvDecoder.setRsaParameters(caCert.modulus(), caCert.modulusSize(), caCert.exponent(), caCert.exponentSize()); - cvDecoder.addWithRecoveredMessage(cvSig->contentData(), cvSig->contentSize()); - cvDecoder.add(cvRem->contentData(), cvRem->contentSize()); - const auto cvDecoded = cvDecoder.recoveredMessage(); - if (cvDecoded.isEmpty()) { + cvCert.setCaCertificate(caCert); + if (!cvCert.isValid()) { qDebug() << "Failed to decode CV certificate."; return; } // (3) decode the ticket data using the decoded CV certificate - // TODO + const auto sig = reinterpret_cast(data.constData()); + qDebug() << sig->isValid() << sig->contentSize(); + + Iso9796_2Decoder decoder; + decoder.setRsaParameters(cvCert.modulus(), cvCert.modulusSize(), cvCert.exponent(), cvCert.exponentSize()); + decoder.addWithRecoveredMessage(sig->contentData(), sig->contentSize()); + decoder.add(sigRemainder->contentData(), sigRemainder->contentSize()); // (4) profit! + qDebug() << decoder.recoveredMessage(); + qDebug() << decoder.recoveredMessage().toHex(); // TODO } bool VdvTicketParser::maybeVdvTicket(const QByteArray& data) { if (data.size() < 352) { return false; } // signature header if ((uint8_t)data[0] != TagSignature || (uint8_t)data[1] != 0x81 || (uint8_t)data[2] != 0x80 || (uint8_t)data[VdvSignatureRemainder::Offset] != TagSignatureRemainder) { return false; } const uint8_t len = data[132]; // length of the 0x9A unsigned data block if (len + 133 > data.size()) { return false; } // verify the "VDV" marker is there return strncmp(data.constData() + 133 + len - 5, "VDV", 3) == 0; }