Commit 30170704 authored by Volker Krause's avatar Volker Krause
Browse files

Rebase the VDV ticket parser on top of BER::Element

This cleanly separates VDV specific semantics from general BER decoding,
and cleans up the various incomplete/adhoc BER decoding hacks in the VDV
code.
parent 04097d32
......@@ -126,6 +126,11 @@ int BER::Element::size() const
return s;
}
const char *BER::Element::rawData() const
{
return m_data.constData() + m_offset;
}
int BER::Element::contentSize() const
{
const auto ts = typeSize();
......
......@@ -47,6 +47,10 @@ public:
/** Size of the entire element (type, size and content). */
int size() const;
/** Raw data of this element.
* Typically only needed when copying/writing this element somewhere.
*/
const char* rawData() const;
/** Size of the value part of this element.
* This is excluding a possible variable length end marker.
......@@ -83,8 +87,10 @@ private:
};
template <uint32_t TagValue>
struct TypedElement : public Element
class TypedElement : public Element
{
public:
using Element::Element;
inline bool isValid() const
{
return Element::isValid() && type() == TagValue;
......
......@@ -6,6 +6,7 @@ add_executable(vdv-cert-downloader
cert-downloader.cpp
../vdvcertificate.cpp
../iso9796_2decoder.cpp
../../tlv/berelement.cpp
)
target_include_directories(vdv-cert-downloader PRIVATE ${CMAKE_BINARY_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/..)
......
......@@ -30,20 +30,15 @@ VdvCertificate::VdvCertificate() = default;
VdvCertificate::VdvCertificate(const QByteArray &data, int offset)
: m_offset(offset)
{
if ((unsigned)data.size() <= m_offset + sizeof(VdvCertificateHeader)) {
qDebug() << "Certificate data too small:" << data.size() << offset;
const auto hdr = BER::TypedElement<TagCertificate>(data, offset);
if (!hdr.isValid()) {
qDebug() << "Invalid certificate header:" << hdr.isValid() << data.size() << offset;
return;
}
m_data = data;
const auto hdr = header();
if (!hdr->isValid() || data.size() < hdr->size() + offset) {
qDebug() << "Invalid certificate header:" << hdr->isValid() << hdr->size() << data.size() << offset;
m_data.clear();
return;
}
const auto certKeyBlock = hdr->contentAt<VdvCertificateKeyBlock>(0);
if (certKeyBlock->isValid()) {
const auto certKeyBlock = hdr.find(TagCertificateContent);
if (certKeyBlock.isValid()) {
m_type = Raw;
qDebug() << "found decrypted key";
qDebug() << "CHR:" << QByteArray(certKey()->chr.name, 5) << certKey()->chr.algorithmReference << certKey()->chr.year;
......@@ -51,8 +46,8 @@ VdvCertificate::VdvCertificate(const QByteArray &data, int offset)
return;
}
const auto sig = hdr->contentAt<VdvCertificateSignature>(0);
if (!sig->isValid()) {
const auto sig = hdr.find(TagCertificateSignature);
if (!sig.isValid()) {
qWarning() << "Invalid certificate content: neither a key nor a signature!";
m_data.clear();
return;
......@@ -79,7 +74,7 @@ bool VdvCertificate::needsCaKey() const
int VdvCertificate::size() const
{
return m_type == Invalid ? 0 : header()->size();
return m_type == Invalid ? 0 : header().size();
}
uint16_t VdvCertificate::modulusSize() const
......@@ -122,15 +117,15 @@ void VdvCertificate::setCaCertificate(const VdvCertificate &caCert)
Iso9796_2Decoder decoder;
decoder.setRsaParameters(caCert.modulus(), caCert.modulusSize(), caCert.exponent(), caCert.exponentSize());
const auto sig = header()->contentAt<VdvCertificateSignature>(0);
decoder.addWithRecoveredMessage(sig->contentData(), sig->contentSize());
const auto sig = header().find(TagCertificateSignature);
decoder.addWithRecoveredMessage(sig.contentData(), sig.contentSize());
if (header()->contentSize() > sig->size()) {
const auto rem = header()->contentAt<VdvCertificateSignatureRemainder>(sig->size());
if (rem->isValid() && rem->size() + sig->size() >= header()->contentSize()) {
decoder.add(rem->contentData(), rem->contentSize());
if (header().contentSize() > sig.size()) {
const auto rem = header().find(TagCertificateSignatureRemainder);
if (rem.isValid()) {
decoder.add(rem.contentData(), rem.contentSize());
} else {
qWarning() << "Invalid signature remainder!" << rem->isValid() << rem->size() << sig->size() << header()->contentSize();
qWarning() << "Invalid signature remainder!" << rem.isValid() << rem.size() << sig.size() << header().contentSize();
}
}
......@@ -142,7 +137,7 @@ void VdvCertificate::setCaCertificate(const VdvCertificate &caCert)
} else {
qWarning() << "decrypting certificate key failed!";
qDebug() << "size is:" << m_recoveredData.size() << "expected:" << (certKey()->headerSize() + modulusSize() + exponentSize());
qDebug() << QByteArray((const char*)sig->contentData(), sig->contentSize()).toHex();;
qDebug() << QByteArray((const char*)sig.contentData(), sig.contentSize()).toHex();
m_type = Invalid;
m_recoveredData.clear();
}
......@@ -170,9 +165,9 @@ void VdvCertificate::writeKey(QIODevice *out) const
writeTaggedSize(out, m_recoveredData.size());
out->write(m_recoveredData);
} else if (m_type == Raw) {
const auto keyBlock = header()->contentAt<VdvCertificateKeyBlock>(0);
writeTaggedSize(out, keyBlock->size());
out->write((const char*)keyBlock, keyBlock->size());
const auto keyBlock = header().find(TagCertificateContent);
writeTaggedSize(out, keyBlock.size());
out->write(keyBlock.rawData(), keyBlock.size());
}
}
......@@ -187,9 +182,9 @@ QDate KItinerary::VdvCertificate::endOfValidity() const
return QDate(key->date.year(), key->date.month(), key->date.day());
}
const VdvCertificateHeader* VdvCertificate::header() const
BER::Element VdvCertificate::header() const
{
return reinterpret_cast<const VdvCertificateHeader*>(m_data.constData() + m_offset);
return BER::Element(m_data, m_offset);
}
const VdvCertificateKey* VdvCertificate::certKey() const
......@@ -197,7 +192,7 @@ const VdvCertificateKey* VdvCertificate::certKey() const
if (m_type == Signed) {
return reinterpret_cast<const VdvCertificateKey*>(m_recoveredData.constData());
} else if (m_type == Raw) {
return header()->contentAt<VdvCertificateKeyBlock>(0)->contentAt<VdvCertificateKey>(0);
return header().find(TagCertificateContent).contentAt<VdvCertificateKey>();
}
return nullptr;
}
......
......@@ -25,8 +25,8 @@ class QIODevice;
namespace KItinerary {
namespace BER { class Element; }
struct VdvCaReference;
struct VdvCertificateHeader;
struct VdvCertificateKey;
/** Certificate object, to obtain the RSA parameters.
......@@ -70,7 +70,7 @@ public:
QDate endOfValidity() const;
private:
const VdvCertificateHeader *header() const;
BER::Element header() const;
const VdvCertificateKey *certKey() const;
QByteArray m_data;
......
......@@ -18,24 +18,21 @@
#ifndef KITINERARY_VDVDATA_P_H
#define KITINERARY_VDVDATA_P_H
#include "../tlv/berelement_p.h"
#include <QtEndian>
#include <cstdint>
namespace KItinerary {
enum : uint8_t {
enum {
TagSignature = 0x9E,
TagSignatureRemainder = 0x9A,
TagCaReference = 0x42,
TagOneByteSize = 0x81,
TagTwoByteSize = 0x82,
TagTicketProductData = 0x85,
TagTicketProductTransactionData = 0x8A,
TagTicketBasicData = 0xDA,
TagTicketTravelerData = 0xDB,
};
enum : uint16_t {
TagCertificate = 0x7F21,
TagCertificateSignature = 0x5F37,
TagCertificateSignatureRemainder = 0x5F38,
......@@ -49,96 +46,6 @@ enum {
#pragma pack(push)
#pragma pack(1)
/** Generic structure for the TLV (tag-length-value) 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 <typename TagType>
struct VdvTlvBlock
{
TagType tag;
uint8_t size0;
inline uint16_t contentSize() const
{
return size0;
}
inline uint16_t contentOffset() const
{
return sizeof(VdvTlvBlock);
}
inline const uint8_t* contentData() const
{
return reinterpret_cast<const uint8_t*>(this) + contentOffset();
}
inline uint16_t size() const
{
return contentSize() + contentOffset();
}
template <typename T>
inline const T* contentAt(int offset) const
{
return reinterpret_cast<const T*>(contentData() + offset);
}
};
template <typename TagType, TagType TagValue>
struct VdvTlvTypedBlock : public VdvTlvBlock<TagType>
{
enum : TagType { Tag = TagValue };
inline bool isValid() const
{
return qFromBigEndian(VdvTlvBlock<TagType>::tag) == TagValue;
}
};
template <typename TagType, TagType TagValue>
struct VdvTaggedSizeDataBlock
{
TagType tag;
uint8_t sizeTag;
uint8_t size0;
uint8_t size1;
inline bool isValid() const
{
return qFromBigEndian(tag) == TagValue && (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<const uint8_t*>(this) + contentOffset();
}
inline uint16_t size() const
{
return contentSize() + contentOffset();
}
template <typename T>
inline const T* contentAt(int offset) const
{
return reinterpret_cast<const T*>(contentData() + offset);
}
};
/** Two-digit BCD encoded number. */
struct VdvBcdNumber
{
......@@ -171,13 +78,6 @@ struct VdvBcdDate
}
};
/** Signature container for the signed part of the payload data. */
struct VdvSignature : public VdvTaggedSizeDataBlock<uint8_t, TagSignature> {};
/** Signature Remainder header. */
struct VdvSignatureRemainder : public VdvTlvTypedBlock<uint8_t, TagSignatureRemainder> {};
/** CV certificate. */
struct VdvCertificateHeader : public VdvTaggedSizeDataBlock<uint16_t, TagCertificate> {};
/** Certificate Authority Reference (CAR) content. */
struct VdvCaReference
{
......@@ -188,7 +88,6 @@ struct VdvCaReference
uint8_t algorithmReference;
uint8_t year;
};
struct VdvCaReferenceBlock : public VdvTlvTypedBlock<uint8_t, TagCaReference> {};
/** Certificate Holder Reference (CHR) */
struct VdvCertificateHolderReference {
......@@ -225,12 +124,6 @@ struct VdvCertificateKey {
return sizeof(VdvCertificateKey) + oidSize() - 1;
}
};
struct VdvCertificateKeyBlock : public VdvTaggedSizeDataBlock<uint16_t, TagCertificateContent> {};
/** Certificate signature. */
struct VdvCertificateSignature : public VdvTaggedSizeDataBlock<uint16_t, TagCertificateSignature> {};
/** Certificate signature remainder. */
struct VdvCertificateSignatureRemainder : public VdvTlvTypedBlock<uint16_t, TagCertificateSignatureRemainder> {};
/** Date/time representation encoded in 4 byte. */
struct VdvDateTimeCompact
......@@ -274,46 +167,8 @@ struct VdvTicketHeader
VdvDateTimeCompact endDt;
};
/** Product-specific ticket data block.
* Contains a set of TLV elements.
*/
struct VdvTicketProductData : public VdvTlvTypedBlock<uint8_t, TagTicketProductData>
{
/** First TLV element inside this block. */
const VdvTlvBlock* first() const
{
if (contentSize() < 2) {
return nullptr;
}
const auto tlv = contentAt<VdvTlvBlock>(0);
return tlv->size() > contentSize() ? nullptr : tlv;
}
/** Next TLV element, for iteration. */
const VdvTlvBlock* next(const VdvTlvBlock *prev) const
{
const auto off = (const uint8_t*)prev - contentData() + prev->size();
if (off + 2 > contentSize()) {
return nullptr;
}
const auto tlv = contentAt<VdvTlvBlock>(off);
return tlv->size() > contentSize() ? nullptr : tlv;
}
/** Find TLV element with type @tparam T. */
template <typename T>
inline const T* contentByTag() const
{
for (auto tlv = first(); tlv; tlv = next(tlv)) {
if (qFromBigEndian(tlv->tag) == T::Tag && tlv->size() >= sizeof(T)) {
return reinterpret_cast<const T*>(tlv);
}
}
return nullptr;
}
};
/** Product specific data - basic information. */
struct VdvTicketBasicData : public VdvTlvTypedBlock<uint8_t, TagTicketBasicData>
struct VdvTicketBasicData
{
uint8_t paymentType;
uint8_t travelerType; // 1 adult, 2 child, 65 bike
......@@ -330,7 +185,7 @@ struct VdvTicketBasicData : public VdvTlvTypedBlock<uint8_t, TagTicketBasicData>
};
/** Product specific data - traveler information. */
struct VdvTicketTravelerData : public VdvTlvTypedBlock<uint8_t, TagTicketTravelerData>
struct VdvTicketTravelerData
{
uint8_t gender;
VdvBcdDate birthDate;
......@@ -340,9 +195,9 @@ struct VdvTicketTravelerData : public VdvTlvTypedBlock<uint8_t, TagTicketTravele
{
return &nameBegin;
}
inline int nameSize() const
inline int nameSize(int elementSize) const
{
return size() - sizeof(VdvTicketTravelerData) + 1;
return elementSize - sizeof(VdvTicketTravelerData) + 1;
}
};
......@@ -355,9 +210,6 @@ struct VdvTicketTransactionData
uint8_t locationId[6];
};
/** Product-specific transaction data block (variable length). */
struct VdvTicketProductTransactionData : public VdvTlvTypedBlock<uint8_t, TagTicketProductTransactionData> {};
/** Ticket issuer data block. */
struct VdvTicketIssueData
{
......
......@@ -29,19 +29,28 @@ class VdvTicketPrivate : public QSharedData
public:
QByteArray m_data;
template <typename T> const T* productData() const;
BER::Element productElement(uint32_t type) const;
template <typename T> const T* productData(uint32_t type) const;
};
}
template <typename T>
const T* VdvTicketPrivate::productData() const
BER::Element VdvTicketPrivate::productElement(uint32_t type) const
{
if (m_data.isEmpty()) {
return nullptr;
const auto productElement = BER::TypedElement<TagTicketProductData>(m_data, sizeof(VdvTicketHeader));
if (!productElement.isValid()) {
return {};
}
return productElement.find(type);
}
const auto productBlock = reinterpret_cast<const VdvTicketProductData*>(m_data.constData() + sizeof(VdvTicketHeader));
return productBlock->contentByTag<T>();
template <typename T>
const T* VdvTicketPrivate::productData(uint32_t type) const
{
const auto elem = productElement(type);
if (elem.isValid()) {
return elem.template contentAt<T>();
}
return nullptr;
}
VdvTicket::VdvTicket()
......@@ -60,23 +69,23 @@ VdvTicket::VdvTicket(const QByteArray &data)
static_assert(sizeof(VdvTicketHeader) < MinimumTicketDataSize, "");
int offset = sizeof(VdvTicketHeader);
const auto productBlock = reinterpret_cast<const VdvTicketProductData*>(data.constData() + offset);
if (!productBlock->isValid() || productBlock->size() + offset > data.size()) {
qCWarning(Log) << "Invalid product block" << productBlock->isValid() << productBlock->size() << offset << data.size();
const auto productBlock = BER::TypedElement<TagTicketProductData>(data, offset);
if (!productBlock.isValid() || productBlock.size() + offset > data.size()) {
qCWarning(Log) << "Invalid product block" << productBlock.isValid() << productBlock.size() << offset << data.size();
return;
}
offset += productBlock->size();
offset += productBlock.size();
const auto transactionBlock = reinterpret_cast<const VdvTicketTransactionData*>(data.constData() + offset);
qDebug() << "transaction block:" << qFromBigEndian(transactionBlock->kvpOrgId);
offset += sizeof(VdvTicketTransactionData);
const auto prodTransactionBlock = reinterpret_cast<const VdvTicketProductTransactionData*>(data.constData() + offset);
if (!prodTransactionBlock->isValid() || prodTransactionBlock->size() + offset > data.size()) {
qCWarning(Log) << "Invalid product transaction block" << prodTransactionBlock->isValid() << prodTransactionBlock->size() << offset << data.size();
const auto prodTransactionBlock = BER::TypedElement<TagTicketProductTransactionData>(data, offset);
if (!prodTransactionBlock.isValid()) {
qCWarning(Log) << "Invalid product transaction block" << prodTransactionBlock.isValid() << offset << data.size();
return;
}
offset += prodTransactionBlock->size();
offset += prodTransactionBlock.size();
const auto issueData = reinterpret_cast<const VdvTicketIssueData*>(data.constData() + offset);
qDebug() << issueData->version << QByteArray((const char*)&issueData->samId, 3).toHex();
......@@ -92,16 +101,16 @@ VdvTicket::VdvTicket(const QByteArray &data)
}
d->m_data = data;
#if 0
#if 1
const auto hdr = reinterpret_cast<const VdvTicketHeader*>(data.constData());
qDebug() << qFromBigEndian(hdr->productId) << qFromBigEndian(hdr->pvOrgId);
// iterate over TLV content
auto tlv = productBlock->first();
while (tlv) {
qDebug() << "tag:" << tlv->tag << "size:" << tlv->contentSize() << "content:" << QByteArray((const char*)tlv->contentData(), tlv->contentSize()).toHex();
tlv = productBlock->next(tlv);
auto tlv = productBlock.first();
while (tlv.isValid()) {
qDebug() << "tag:" << tlv.type() << "size:" << tlv.contentSize() << "content:" << QByteArray((const char*)tlv.contentData(), tlv.contentSize()).toHex();
tlv = tlv.next();
}
const auto basicData = productBlock->contentByTag<VdvTicketBasicData>();
const auto basicData = d->productData<VdvTicketBasicData>(TagTicketBasicData);
if (basicData) {
qDebug() << "traveler type:" << basicData->travelerType;
}
......@@ -149,7 +158,7 @@ int VdvTicket::issuerId() const
VdvTicket::ServiceClass VdvTicket::serviceClass() const
{
const auto tlv = d->productData<VdvTicketBasicData>();
const auto tlv = d->productData<VdvTicketBasicData>(TagTicketBasicData);
if (!tlv) {
return UnknownClass;
}
......@@ -169,13 +178,14 @@ VdvTicket::ServiceClass VdvTicket::serviceClass() const
Person VdvTicket::person() const
{
const auto tlv = d->productData<VdvTicketTravelerData>();
const auto elem = d->productElement(TagTicketTravelerData);
const auto tlv = elem.isValid() ? elem.contentAt<VdvTicketTravelerData>() : nullptr;
if (!tlv) {
return {};
}
qDebug() << "traveler:" << tlv->gender << QDate(tlv->birthDate.year(), tlv->birthDate.month(), tlv->birthDate.day()) << QByteArray(tlv->name(), tlv->nameSize());
qDebug() << "traveler:" << tlv->gender << QDate(tlv->birthDate.year(), tlv->birthDate.month(), tlv->birthDate.day()) << QByteArray(tlv->name(), tlv->nameSize(elem.contentSize()));
const auto len = strnlen(tlv->name(), tlv->nameSize()); // name field can contain null bytes
const auto len = strnlen(tlv->name(), tlv->nameSize(elem.contentSize())); // name field can contain null bytes
if (len == 0) {
return {};
}
......
......@@ -32,31 +32,35 @@ VdvTicketParser::~VdvTicketParser() = default;
bool VdvTicketParser::parse(const QByteArray &data)
{
// (1) find the certificate authority reference (CAR) to identify the key to decode the CV certificate
const auto sig = reinterpret_cast<const VdvSignature*>(data.constData());
if (!sig->isValid()) {
const auto sig = BER::TypedElement<TagSignature>(data);
if (!sig.isValid()) {
qCDebug(Log) << "Invalid VDV ticket signature.";
return false;
}
const auto sigRemainder = reinterpret_cast<const VdvSignatureRemainder*>(data.constData() + sig->size());
if (!sigRemainder->isValid() || sig->size() + sigRemainder->size() + sizeof(VdvCertificateHeader) > (unsigned)data.size()) {
const auto sigRemainder = BER::TypedElement<TagSignatureRemainder>(data, sig.size());
if (!sigRemainder.isValid()) {
qCDebug(Log) << "Invalid VDV signature remainder.";
return false;
}
const auto cvCertOffset = sig->size() + sigRemainder->size();
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()) {
if ((!cvCert.isValid() && !cvCert.needsCaKey())) {
qCDebug(Log) << "Invalid CV signature:" << cvCert.isValid() << cvCertOffset << cvCert.size();
return false;
}
const auto carOffset = cvCertOffset + cvCert.size();
const auto carBlock = reinterpret_cast<const VdvCaReferenceBlock*>(data.constData() + carOffset);
if (!carBlock->isValid() || carBlock->contentSize() < sizeof(VdvCaReference)) {
const auto carBlock = BER::TypedElement<TagCaReference>(data, carOffset);
if (!carBlock.isValid()) {
qCDebug(Log) << "Invalid CA Reference.";
return false;
}
const auto car = carBlock->contentAt<VdvCaReference>(0);
const auto car = carBlock.contentAt<VdvCaReference>(0);
if (!car) {
qCDebug(Log) << "Cannot obtain CA Reference.";
return false;
}
qCDebug(Log) << "CV CAR:" << QByteArray(car->region, 5) << car->serviceIndicator << car->discretionaryData << car->algorithmReference << car->year;
const auto caCert = VdvPkiRepository::caCertificate(car);
......@@ -75,8 +79,8 @@ bool VdvTicketParser::parse(const QByteArray &data)
// (3) decode the ticket data using the decoded CV certificate
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());
decoder.addWithRecoveredMessage(sig.contentData(), sig.contentSize());
decoder.add(sigRemainder.contentData(), sigRemainder.contentSize());
// (4) profit!
m_ticket = VdvTicket(decoder.recoveredMessage());
......@@ -90,17 +94,17 @@ bool VdvTicketParser::maybeVdvTicket(const QByteArray& data)
}
// signature header
const auto sig = reinterpret_cast<const VdvSignature*>(data.constData());
if (!sig->isValid()) {
const auto sig = BER::TypedElement<TagSignature>(data);
if (!sig.isValid()) {
return false;
}
const auto rem = reinterpret_cast<const VdvSignatureRemainder*>(data.constData() + sig->size());
if (!rem->isValid() || sig->size() + rem->size() + sizeof(VdvCertificateHeader) > (unsigned)data.size()) {
const auto rem = BER::TypedElement<TagSignatureRemainder>(data, sig.size());
if (!rem.isValid()) {
return false;
}
// verify the "VDV" marker is there
return strncmp((const char*)(rem->contentData() + rem->contentSize() - 5), "VDV", 3) == 0;
return strncmp((const char*)(rem.contentData() + rem.contentSize() - 5), "VDV", 3) == 0;
}