diff --git a/src/vdv/iso9796_2decoder_p.h b/src/vdv/iso9796_2decoder_p.h index be3cbfc..2e6dafd 100644 --- a/src/vdv/iso9796_2decoder_p.h +++ b/src/vdv/iso9796_2decoder_p.h @@ -1,61 +1,67 @@ /* 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 #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(); + /** Set RSA modulus and exponents (@see VdvCertificate). */ void setRsaParameters(const uint8_t *modulus, uint16_t modulusSize, const uint8_t *exponent, uint16_t exponentSize); + /** Process the first block of data, containing the recoverable message and the SHA-1 hash. */ void addWithRecoveredMessage(const uint8_t *data, int size); + /** Process any further data. */ void add(const uint8_t *data, int size); + /** Returns the recovered message. + * This should either be modulusSize - 22 bytes, or empty in case of an error. + */ 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 8afe810..c027c2c 100644 --- a/src/vdv/vdvcertificate.cpp +++ b/src/vdv/vdvcertificate.cpp @@ -1,198 +1,188 @@ /* 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; + qDebug() << "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; + qDebug() << "Invalid certificate header:" << hdr->isValid() << hdr->size() << data.size() << offset; m_data.clear(); return; } const auto certKeyBlock = hdr->contentAt(0); 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()); + qDebug() << "CHR:" << QByteArray(certKey()->chr.name, 5) << certKey()->chr.algorithmReference << certKey()->chr.year; + qDebug() << "CAR:" << QByteArray(certKey()->car.region, 2) << QByteArray(certKey()->car.name, 3); return; } const auto sig = hdr->contentAt(0); if (!sig->isValid()) { qWarning() << "Invalid certificate content: neither a key nor a signature!"; m_data.clear(); return; } m_type = Signed; qDebug() << "found encrypted key"; } VdvCertificate::~VdvCertificate() = default; bool VdvCertificate::isValid() const { 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 { const auto k = certKey(); return (&k->oidBegin) + k->oidSize(); } uint16_t VdvCertificate::exponentSize() const { return 4; } const uint8_t* VdvCertificate::exponent() const { 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()); + 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; } 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 { 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(const VdvCaReference *car) { 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" << f.fileName() << f.errorString(); return VdvCertificate(); } 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/vdvdata_p.h b/src/vdv/vdvdata_p.h index 7974171..e706b97 100644 --- a/src/vdv/vdvdata_p.h +++ b/src/vdv/vdvdata_p.h @@ -1,204 +1,199 @@ /* 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 }; -}; - +struct VdvSignatureRemainder : public VdvSimpleDataBlock {}; /** CV certificate. */ -struct VdvCertificateHeader : public VdvTaggedSizeDataBlock { -}; +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 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 6d39c88..8cbff25 100644 --- a/src/vdv/vdvticketparser.cpp +++ b/src/vdv/vdvticketparser.cpp @@ -1,109 +1,107 @@ /* 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 "logging.h" #include #include using namespace KItinerary; VdvTicketParser::VdvTicketParser() = default; VdvTicketParser::~VdvTicketParser() = default; -void VdvTicketParser::parse(const QByteArray &data) +bool 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; + const auto sig = reinterpret_cast(data.constData()); + if (!sig->isValid()) { + qCDebug(Log) << "Invalid VDV ticket signature."; + return false; + } + const auto sigRemainder = reinterpret_cast(data.constData() + sig->size()); + if (!sigRemainder->isValid() || sig->size() + sigRemainder->size() + sizeof(VdvCertificateHeader) > (unsigned)data.size()) { + qCDebug(Log) << "Invalid VDV signature remainder."; + return false; } - qDebug() << sigRemainder->contentSize(); - const auto cvCertOffset = VdvSignatureRemainder::Offset + sigRemainder->size(); - auto cvCert = VdvCertificate(data ,cvCertOffset); + const auto cvCertOffset = sig->size() + sigRemainder->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; + qCDebug(Log) << "Invalid CV signature:" << cvCert.isValid() << cvCertOffset << cvCert.size(); + return false; } 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; + qCDebug(Log) << "Invalid CA Reference."; + return false; } const auto car = carBlock->contentAt(0); - qDebug() << QByteArray(car->name, 3) << car->serviceIndicator << car->discretionaryData << car->algorithmReference << car->year; + qCDebug(Log) << "CV CAR:" << QByteArray(car->region, 5) << car->serviceIndicator << car->discretionaryData << car->algorithmReference << car->year; const auto caCert = VdvPkiRepository::caCertificate(car); if (!caCert.isValid()) { - qWarning() << "Could not find CA certificate" << QByteArray(reinterpret_cast(car), sizeof(VdvCaReference)).toHex(); - return; + qCWarning(Log) << "Could not find CA certificate" << QByteArray(reinterpret_cast(car), sizeof(VdvCaReference)).toHex(); + return false; } // (2) decode the CV certificate cvCert.setCaCertificate(caCert); if (!cvCert.isValid()) { - qDebug() << "Failed to decode CV certificate."; - return; + qCWarning(Log) << "Failed to decode CV certificate."; + return false; } // (3) decode the ticket data using the decoded CV certificate - 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 + return true; } 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) { + const auto sig = reinterpret_cast(data.constData()); + if (!sig->isValid()) { return false; } - - const uint8_t len = data[132]; // length of the 0x9A unsigned data block - if (len + 133 > data.size()) { + const auto rem = reinterpret_cast(data.constData() + sig->size()); + if (!rem->isValid() || sig->size() + rem->size() + sizeof(VdvCertificateHeader) > (unsigned)data.size()) { return false; } // verify the "VDV" marker is there - return strncmp(data.constData() + 133 + len - 5, "VDV", 3) == 0; + return strncmp((const char*)(rem->contentData() + rem->contentSize() - 5), "VDV", 3) == 0; } diff --git a/src/vdv/vdvticketparser.h b/src/vdv/vdvticketparser.h index 038aad9..9a8f29d 100644 --- a/src/vdv/vdvticketparser.h +++ b/src/vdv/vdvticketparser.h @@ -1,56 +1,57 @@ /* 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_VDVTICKETPARSER_H #define KITINERARY_VDVTICKETPARSER_H #include "kitinerary_export.h" class QByteArray; namespace KItinerary { /** Parser for VDV tickets. * Or more correctly for: "Statische Berechtigungen der VDV-Kernapplikation" * That is, a standard for 2D barcode tickets for local public transport, commonly found in Germany * and some neighbouring countries. * * This is based on "VDV-Kernapplikation - Spezifikation statischer Berechtigungen für 2D Barcode-Tickets" * which your favorite search engine should find as a PDF. * * The crypto stuff used here is ISO 9796-2, and you'll find some terminology also used in ISO 7816-6/8, * which isn't entirely surprising given this also exists in a NFC card variant. * * Do not use directly, only installed for use in tooling. */ class KITINERARY_EXPORT VdvTicketParser { public: VdvTicketParser(); ~VdvTicketParser(); - void parse(const QByteArray &data); + /** Tries to parse the ticket in @p data. */ + bool parse(const QByteArray &data); /** Fast check if @p data might contain a VDV ticket. * Does not perform full decoding, mainly useful for content auto-detection. */ static bool maybeVdvTicket(const QByteArray &data); }; } #endif // KITINERARY_VDVTICKETPARSER_H