diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bbacdd1..02c1615 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,233 +1,235 @@ set(KDE_INSTALL_INCLUDEDIR_PIM ${KDE_INSTALL_INCLUDEDIR}/KPim) add_subdirectory(cli) if (TARGET Qt5::Network) add_subdirectory(knowledgedb-generator) endif() configure_file(config-kitinerary.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kitinerary.h) set(kitinerary_lib_srcs datatypes/action.cpp datatypes/brand.cpp datatypes/bustrip.cpp datatypes/creativework.cpp datatypes/event.cpp datatypes/flight.cpp datatypes/organization.cpp datatypes/person.cpp datatypes/place.cpp datatypes/reservation.cpp datatypes/taxi.cpp datatypes/ticket.cpp datatypes/traintrip.cpp datatypes/rentalcar.cpp datatypes/visit.cpp generic/genericpdfextractor.cpp generic/genericpkpassextractor.cpp generic/genericuic918extractor.cpp generic/structureddataextractor.cpp jsapi/barcode.cpp jsapi/bitarray.cpp jsapi/context.cpp jsapi/jsonld.cpp knowledgedb/alphaid.cpp knowledgedb/airportdb.cpp knowledgedb/countrydb.cpp knowledgedb/iatacode.cpp knowledgedb/knowledgedb.cpp knowledgedb/timezonedb.cpp knowledgedb/trainstationdb.cpp pdf/pdfdocument.cpp pdf/pdfextractoroutputdevice.cpp pdf/pdfimage.cpp pdf/pdfvectorpicture.cpp pdf/popplerglobalparams.cpp pdf/popplerutils.cpp uic9183/rct2ticket.cpp uic9183/uic9183block.cpp uic9183/uic9183parser.cpp uic9183/uic9183ticketlayout.cpp uic9183/vendor0080block.cpp + vdv/vdvcertificate.cpp vdv/vdvticketparser.cpp + vdv/certs/vdv-certs.qrc barcodedecoder.cpp calendarhandler.cpp documentutil.cpp extractor.cpp extractorengine.cpp extractorfilter.cpp extractorinput.cpp extractorpostprocessor.cpp extractorrepository.cpp extractorutil.cpp extractorvalidator.cpp file.cpp flightpostprocessor.cpp htmldocument.cpp iatabcbpparser.cpp jsonlddocument.cpp jsonldimportfilter.cpp locationutil.cpp mergeutil.cpp qimagepurebinarizer.cpp sortutil.cpp stringutil.cpp ) qt5_add_resources(kitinerary_lib_srcs extractors/extractors.qrc) ecm_qt_declare_logging_category(kitinerary_lib_srcs HEADER logging.h IDENTIFIER KItinerary::Log CATEGORY_NAME org.kde.kitinerary) ecm_qt_declare_logging_category(kitinerary_lib_srcs HEADER compare-logging.h IDENTIFIER KItinerary::CompareLog CATEGORY_NAME org.kde.kitinerary.comparator) ecm_qt_declare_logging_category(kitinerary_lib_srcs HEADER validator-logging.h IDENTIFIER KItinerary::ValidatorLog CATEGORY_NAME org.kde.kitinerary.extractorValidator) add_library(KPimItinerary ${kitinerary_lib_srcs}) add_library(KPim::Itinerary ALIAS KPimItinerary) generate_export_header(KPimItinerary BASE_NAME KItinerary) set_target_properties(KPimItinerary PROPERTIES VERSION ${KITINERARY_VERSION_STRING} SOVERSION ${KITINERARY_SOVERSION} EXPORT_NAME Itinerary ) target_include_directories(KPimItinerary INTERFACE "$") target_include_directories(KPimItinerary PUBLIC "$") target_link_libraries(KPimItinerary PUBLIC Qt5::Core KF5::Mime PRIVATE Qt5::Qml KF5::Archive KF5::I18n KF5::Contacts KPim::PkPass ${ZLIB_LIBRARIES} ) if (HAVE_POPPLER) target_link_libraries(KPimItinerary PRIVATE Poppler::Core) endif() if (HAVE_ZXING) target_link_libraries(KPimItinerary PRIVATE ZXing::Core) endif() if (HAVE_KCAL) target_link_libraries(KPimItinerary PUBLIC KF5::CalendarCore) endif() if (HAVE_LIBXML2) target_compile_definitions(KPimItinerary PRIVATE ${LIBXML2_DEFINITIONS}) target_include_directories(KPimItinerary PRIVATE ${LIBXML2_INCLUDE_DIR}) target_link_libraries(KPimItinerary PRIVATE ${LIBXML2_LIBRARIES}) endif() if (HAVE_PHONENUMBER) target_link_libraries(KPimItinerary PRIVATE PhoneNumber::PhoneNumber) endif() ecm_generate_headers(KItinerary_FORWARDING_HEADERS HEADER_NAMES BarcodeDecoder CalendarHandler DocumentUtil Extractor ExtractorEngine ExtractorFilter ExtractorInput ExtractorPostprocessor ExtractorRepository File HtmlDocument IataBcbpParser JsonLdDocument LocationUtil MergeUtil SortUtil PREFIX KItinerary REQUIRED_HEADERS KItinerary_HEADERS ) ecm_generate_headers(KItinerary_KnowledgeDb_FORWARDING_HEADERS HEADER_NAMES AlphaId CountryDb KnowledgeDb PREFIX KItinerary REQUIRED_HEADERS KItinerary_KnowledgeDb_HEADERS RELATIVE knowledgedb ) ecm_generate_headers(KItinerary_Datatypes_FORWARDING_HEADERS HEADER_NAMES Action Brand BusTrip CreativeWork Datatypes Event Flight Organization Reservation RentalCar Person Place Taxi Ticket TrainTrip Visit PREFIX KItinerary REQUIRED_HEADERS KItinerary_Datatypes_HEADERS RELATIVE datatypes ) ecm_generate_headers(KItinerary_Pdf_FORWARDING_HEADERS HEADER_NAMES PdfDocument PdfImage PREFIX KItinerary REQUIRED_HEADERS KItinerary_Pdf_HEADERS RELATIVE pdf ) ecm_generate_headers(KItinerary_Uic9183_FORWARDING_HEADERS HEADER_NAMES Rct2Ticket Uic9183Block Uic9183Parser Uic9183TicketLayout Vendor0080Block PREFIX KItinerary REQUIRED_HEADERS KItinerary_Uic9183_HEADERS RELATIVE uic9183 ) ecm_generate_headers(KItinerary_Vdv_FORWARDING_HEADERS HEADER_NAMES VdvTicketParser PREFIX KItinerary REQUIRED_HEADERS KItinerary_Vdv_HEADERS RELATIVE vdv ) install(TARGETS KPimItinerary EXPORT KPimItineraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${KItinerary_FORWARDING_HEADERS} ${KItinerary_KnowledgeDb_FORWARDING_HEADERS} ${KItinerary_Datatypes_FORWARDING_HEADERS} ${KItinerary_Pdf_FORWARDING_HEADERS} ${KItinerary_Uic9183_FORWARDING_HEADERS} ${KItinerary_Vdv_FORWARDING_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/KItinerary ) install(FILES ${KItinerary_HEADERS} ${KItinerary_AirportDb_HEADERS} ${KItinerary_Datatypes_HEADERS} ${KItinerary_KnowledgeDb_HEADERS} ${KItinerary_Pdf_HEADERS} ${KItinerary_Uic9183_HEADERS} ${KItinerary_Vdv_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kitinerary_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/kitinerary ) if (NOT ANDROID) install(FILES application-vnd-kde-itinerary.xml DESTINATION ${XDG_MIME_INSTALL_DIR}) update_xdg_mimetypes(${XDG_MIME_INSTALL_DIR}) endif() diff --git a/src/vdv/certs/2.vdv-cert b/src/vdv/certs/2.vdv-cert new file mode 100644 index 0000000..b8a558d Binary files /dev/null and b/src/vdv/certs/2.vdv-cert differ diff --git a/src/vdv/certs/8.vdv-cert b/src/vdv/certs/8.vdv-cert new file mode 100644 index 0000000..f6faefd Binary files /dev/null and b/src/vdv/certs/8.vdv-cert differ diff --git a/src/vdv/certs/NOTES b/src/vdv/certs/NOTES new file mode 100644 index 0000000..9801a33 --- /dev/null +++ b/src/vdv/certs/NOTES @@ -0,0 +1,5 @@ +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 + +TODO: do this automatically for all relevant certs diff --git a/src/vdv/certs/vdv-certs.qrc b/src/vdv/certs/vdv-certs.qrc new file mode 100644 index 0000000..4d9c067 --- /dev/null +++ b/src/vdv/certs/vdv-certs.qrc @@ -0,0 +1,6 @@ + + + 2.vdv-cert + 8.vdv-cert + + diff --git a/src/vdv/vdvcertificate.cpp b/src/vdv/vdvcertificate.cpp new file mode 100644 index 0000000..ca4d68f --- /dev/null +++ b/src/vdv/vdvcertificate.cpp @@ -0,0 +1,108 @@ +/* + 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 +#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; + } + + qDebug() << "key:" << certKey()->isValid(); + qDebug() << "car:" << QByteArray(certKey()->car.region, 2) << QByteArray(certKey()->car.name, 3); + qDebug() << "chr:" << QByteArray(certKey()->chr.name, 5); + qDebug() << "cha:" << QByteArray(certKey()->cha.name, 6); + qDebug() << "modulus:" << modulusSize() << *modulus() << *(modulus() + modulusSize() - 1); + qDebug() << "exponent:" << exponentSize() << *exponent() << *(exponent() + exponentSize() - 1); +} + +VdvCertificate::~VdvCertificate() = default; + +bool VdvCertificate::isValid() const +{ + return !m_data.isEmpty(); +} + +uint16_t VdvCertificate::modulusSize() const +{ + switch (certKey()->certificateProfileIdentifier) { + case 3: + return 1536 / 8; + case 4: + return 1024 / 8; + } + qWarning() << "Unknown certificate profile identifier: " << certKey()->certificateProfileIdentifier; + return 0; +} + +const uint8_t* VdvCertificate::modulus() const +{ + return &(certKey()->modulusBegin); +} + +uint16_t VdvCertificate::exponentSize() const +{ + return 4; +} + +const uint8_t* VdvCertificate::exponent() const +{ + return &(certKey()->modulusBegin) + modulusSize(); +} + +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 reinterpret_cast(m_data.constData() + m_offset + header()->contentOffset()); +} + + +VdvCertificate VdvPkiRepository::caCertificate(uint8_t serNum) +{ + QFile f(QLatin1String(":/org.kde.pim/kitinerary/vdv/certs/") + QString::number(serNum) + QLatin1String(".vdv-cert")); + if (!f.open(QFile::ReadOnly)) { + qWarning() << "Failed to open CA cert file" << serNum << f.errorString(); + return VdvCertificate(); + } + + qDebug() << f.size(); + return VdvCertificate(f.readAll()); +} diff --git a/src/vdv/vdvcertificate_p.h b/src/vdv/vdvcertificate_p.h new file mode 100644 index 0000000..f9b3e82 --- /dev/null +++ b/src/vdv/vdvcertificate_p.h @@ -0,0 +1,65 @@ +/* + 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 VdvCertificateHeader; +struct VdvCertificateKey; + +/** Certificate object, to obtain the RSA parameters. */ +class VdvCertificate +{ +public: + VdvCertificate(); + explicit VdvCertificate(const QByteArray &data, int offset = 0); + ~VdvCertificate(); + + bool isValid() 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; + +private: + const VdvCertificateHeader *header() const; + const VdvCertificateKey *certKey() const; + + QByteArray m_data; + int m_offset = 0; +}; + +/** VDV (sub)CA certificate access. */ +namespace VdvPkiRepository +{ + /** Returns the (sub)CA certificate for the given serial number. */ + VdvCertificate caCertificate(uint8_t serNum); +} + +} + +#endif // KITINERARY_VDVCERTIFICATE_H diff --git a/src/vdv/vdvdata_p.h b/src/vdv/vdvdata_p.h index 447d606..7b943a8 100644 --- a/src/vdv/vdvdata_p.h +++ b/src/vdv/vdvdata_p.h @@ -1,153 +1,167 @@ /* 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 { - TagCvCertificate = 0x7F21, - TagCvCertificateSignature = 0x5F37, - TagCvCertificateContent = 0x5F4E, + TagCertificate = 0x7F21, + TagCertificateSignature = 0x5F37, + TagCertificateContent = 0x5F4E, }; #pragma pack(push) #pragma pack(1) /** Signature container for the signed part of the payload data. */ struct VdvSignature { uint8_t tag; uint8_t stuff; // always 0x81 uint8_t size; // always 0x80 uint8_t data[128]; }; /** Signature Remainder header. */ struct VdvSignatureRemainder { enum { Offset = 131 }; uint8_t tag; uint8_t contentSize; // >= 5 // followed by size bytes with the remainder of the signed payload data. */ inline bool isValid() const { return tag == TagSignatureRemainder && contentSize >= 5; } inline uint8_t size() const { return contentSize + sizeof(tag) + sizeof(contentSize); } }; /** CV certificate. */ -struct VdvCvCertificate { +struct VdvCertificateHeader { uint16_t tag; + uint8_t sizeTag; uint8_t size0; uint8_t size1; inline bool isValid() const { - return qFromBigEndian(tag) == TagCvCertificate; + return qFromBigEndian(tag) == TagCertificate && (sizeTag == TagOneByteSize || sizeTag == TagTwoByteSize); } inline uint16_t contentSize() const { - return ((size0 << 8) | size1) - 0x8100; + return sizeTag == TagOneByteSize ? size0 : ((size0 << 8) + size1); + } + + inline uint16_t contentOffset() const + { + return sizeof(VdvCertificateHeader) - ((sizeTag == TagOneByteSize) ? 1 : 0); } inline uint16_t size() const { - return contentSize() + sizeof(tag) + sizeof(size0) + sizeof(size1); + return contentSize() + contentOffset(); } }; -/** Certificate Authority Reference (CAR) */ -struct VdvCAReference { - uint8_t tag; - uint8_t contentSize; +/** Certificate Authority Reference (CAR) content. */ +struct VdvCaReferenceContent +{ char region[2]; char name[3]; uint8_t serviceIndicator: 4; uint8_t discretionaryData: 4; uint8_t algorithmReference; uint8_t year; +}; + +struct VdvCaReference { + uint8_t tag; + uint8_t contentSize; + VdvCaReferenceContent car; inline bool isValid() const { return tag == TagCaReference && contentSize == 8; } }; /** Certificate Holder Reference (CHR) */ struct VdvCertificateHolderReference { uint8_t filler[4]; // always null char name[5]; uint8_t extension[3]; }; /** Certificate Holder Authorization (CHA) */ struct VdvCertificateHolderAuthorization { char name[6]; uint8_t stuff; }; /** Certificate key, contained in a certificate object. */ struct VdvCertificateKey { uint16_t tag; - uint16_t taggedSize; - uint8_t cpi; - VdvCAReference car; + uint8_t sizeTag; + uint8_t size0; + uint8_t certificateProfileIdentifier; + VdvCaReferenceContent car; VdvCertificateHolderReference chr; VdvCertificateHolderAuthorization cha; - uint8_t date[3]; + uint8_t date[4]; uint8_t oid[9]; uint8_t modulusBegin; inline bool isValid() const { - return qFromBigEndian(tag) == TagCvCertificateContent; + return qFromBigEndian(tag) == TagCertificateContent && sizeTag == TagOneByteSize; } }; /** Certificate signature. */ struct VdvCertificateSignature { uint16_t tag; uint16_t taggedSize; inline bool isValid() const { - return qFromBigEndian(tag) == TagCvCertificateSignature; + return qFromBigEndian(tag) == TagCertificateSignature; } }; #pragma pack(pop) } #endif // KITINERARY_VDVDATA_P_H diff --git a/src/vdv/vdvticketparser.cpp b/src/vdv/vdvticketparser.cpp index 737c883..e6b48b1 100644 --- a/src/vdv/vdvticketparser.cpp +++ b/src/vdv/vdvticketparser.cpp @@ -1,89 +1,96 @@ /* 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 #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(VdvCvCertificate) > (unsigned)data.size()) { + 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(VdvCAReference) > (unsigned)data.size()) { - qWarning() << "Invalid CV signature."; + const auto cvCert = reinterpret_cast(data.constData() + cvCertOffset); + if (!cvCert->isValid() || cvCertOffset + cvCert->size() + sizeof(VdvCaReference) > (unsigned)data.size()) { + qWarning() << "Invalid CV signature:" << cvCert->isValid() << cvCertOffset << cvCert->size(); return; } qDebug() << cvCert->contentSize(); const auto carOffset = cvCertOffset + cvCert->size(); - const auto car = reinterpret_cast(data.constData() + carOffset); + const auto car = reinterpret_cast(data.constData() + carOffset); if (!car->isValid()) { qWarning() << "Invalid CA Reference."; return; } - qDebug() << QByteArray(car->name, 3) << car->serviceIndicator << car->discretionaryData << car->algorithmReference << car->year; + qDebug() << QByteArray(car->car.name, 3) << car->car.serviceIndicator << car->car.discretionaryData << car->car.algorithmReference << car->car.year; + + const auto caCert = VdvPkiRepository::caCertificate(car->car.algorithmReference); + if (!caCert.isValid()) { + qWarning() << "Could not find CA certificate" << car->car.algorithmReference; + return; + } // (2) decode the CV certificate // TODO // (3) decode the ticket data using the decoded CV certificate // TODO // (4) profit! // 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; }