Работа с картами тахографа.

В данной статье я коротко постараюсь описать, что такое карты тахографа, структру данных на карте, и как считать и разобрать информацию с неё. Все примеры будут рассматриваться на карте водителя, но справедливы и для остальных.

Что такое карта тахографа.

С помощью неё в Европе и РФ контролируют режим работы и отдыха водителей, с целью не нарушения им установленных законом норм.

С технической точки зрения это смарт-карта на которую тахограф записывает некоторые данные, о том как водитель управлял транспортным средством, были ли сбои в работе тахографа и т.д. Каждая карта выдаётся на конкретного водителя, и содержит его данные такие как ФИО, дату рождения, персональный сертификат(причём для Европы и РФ они отличаются). Также на карте содержится закрытый ключ, с помощью которого можно подписать данные с карты при выгрузке, с целью дальней защиты их от фальсификаций.

Корме карты водителя, есть также:

  • карты мастерской (для настройки и калибровки тахографа)
  • карты предприятия
  • карты контролера (с помощью неё можно получить информацию о режимах работы через тахограф)

Далее все рассмотрение будем проводить для карты водителя, так как она самая распространённая.

Структура карты тахографа.

Данные на карте хранятся в виде бинарных файлов, для каждой секции свой. В европейская карта водителя имеет следующую структуру:

Дерево файлов карты тахографа

Описывать все структуры данных в файлах я не буду, так как её можно посмотреть в документации.

Я бы отметил файл EF Driver_Activity_Data, т.к. это едиственная секция в которую входят структуры данных с переменной длиной, а сама структура секции циклична, то надо быть внимательным при ее разборе, для избежания различных сторонних эффектов.

Также надо сказать что в секции EF Application_Identification, содержит данные от которых зависит длина секций с переменной длиной, таких как Я бы отметил файл EF Driver_Activity_Data, EF Events_Data, EF Faults_Data и других.

Чтение данных с карты.

Данные с карты можно прочитать либо с помощью тахографа или же специального ПО при помощи смарт-картридера.

С технической точки зрения более интересен второй способ. Внутри нашей карты стоит собственная ОС которая реагирует на некоторые команды. Все команды нет смысла рассматривать, так как они есть в документации. Поэтому рассмотрим основные:

  • Выбор файла (секции)
  • Чтение файла
  • Цифровая подпись

Все команды отправляются на карту в виде последовательности байт. Для примера реализации я выбрал язык Go. Работа с картой будет использоваться библиотека SCard, которая является врапером pcsc-lite.

Для начала опишем функцию, для отправки команды на карту:

func sendApdu(cmd []byte, card *scard.Card) ([]byte, error) {
	var sw_idx, resp_len int
	var result, sw_byte []byte

	card_resp, err := card.Transmit(cmd)

	if err != nil {
		return nil, err
	}

	resp_len = len(card_resp)
	sw_idx = resp_len - 2

	sw_byte = card_resp[sw_idx:]
	result = card_resp[:sw_idx]

	if !(sw_byte[0] == 0x90 && sw_byte[1] == 0x00) {
		error_msg := fmt.Sprintf("Ошибка выполнения команды % x. Код ошибки: %x.\n", cmd, sw_byte)
		return nil, errors.New(error_msg)
	}

	return result, err
}

Функция принимает на вход команду и указатель на устройство с картой для чтения. На выходе она возвращает либо ошибку, либо результат. Карта присылает ответ в следующем формате: 2 байта статуса операции(90 00 если операция завершилась удачно) затем набор байт результата (если он есть).

Порядок чтения файла следующий: выбираем файл -> читаем содержимое -> подписываем (если нужно).

Команда для выбора файла выглядит так:

00 A4 04 0C 06 <file id>

Напиример для чтения файла EF ICC команда будет следующей: 00 A4 04 0C 00 02

Также, как видно по структуре, есть необходимость выбрать секцию, команда для нее следующая:

00 A4 02 0C 02

Итоговая функция будет выглядеть следующим образом:

func selectFile(fid []byte, card *scard.Card) ([]byte, error) {
	var cmd []byte

	if fid[0] == 0xFF {
		cmd = []byte{0x00, 0xA4, 0x04, 0x0C, 0x06}
	} else {
		cmd = []byte{0x00, 0xA4, 0x02, 0x0C, 0x02}
	}

	cmd = append(cmd, fid...)
	return sendApdu(cmd, card)
}

После того как файл выбран, его можно считать отправив следующий набор байт:

00 B0 <индекс первого байта> <индекс последнего байта>  <длина>

Надо отметить что команда выполняется сразу после выбора файла, и за один проход не может считать больше 255 байт (0xFF). При удачном выполнении команда вернет набор байт, заданной для считывания длины. Полность функция для чтения выглядит так:

func readBinary(size int, card *scard.Card) ([]byte, error) {
	READ_BLOCK_SIZE := 200
	pos := 0
	cmd := []byte{}
	val := []byte{}
	tmp_val := []byte{}
	var expected, begin_byte, end_byte byte
	var err error

	for pos < size {
		if size-pos < READ_BLOCK_SIZE {
			expected = int8ToByte(size - pos)
		} else {
			expected = int8ToByte(READ_BLOCK_SIZE)
		}

		begin_byte = int8ToByte(pos >> 8 & 0xFF)
		end_byte = int8ToByte(pos & 0xFF)
		cmd = []byte{0x00, 0xB0, begin_byte, end_byte, expected}
		tmp_val, err = sendApdu(cmd, card)

		if err != nil {
			return nil, err
		}

		val = append(val, tmp_val...)
		pos = pos + len(tmp_val)
	}

	return val, err
}

После выбора секции и чтения файла осталось разобраться с подписью. Подпись считанного файла делается в 2 этапа:

  1. Сначала происходит генерация хэша, командой:

    80 2A 90 00
    
  2. Затем вычисляется сама подпись, командой:

    00 2A 9E 9A 40
    

    Данная функция вернет набор байт подписи.

Код функций следующий:

func performHash(card *scard.Card) ([]byte, error) {
	cmd := []byte{0x80, 0x2A, 0x90, 0x00}
	return sendApdu(cmd, card)
}

func computeDigitalSignature(card *scard.Card) ([]byte, error) {
	cmd := []byte{0x00, 0x2A, 0x9E, 0x9A, 0x40}
	return sendApdu(cmd, card)
}

По спецификации данные с карты должны выгружаться в TLV формате. Формат имеет следующий вид:

<tag><len><value>

где

  • tag равен имени файла (2 байта) + байта подписи (00-данные, 01 - подпись);
  • len 2 байта, в которых записывается длинна считанного файла или подписи;
  • value значение считанного файла или подписи.

У не подписанного файла будет только одна структура TLV, у подписанного соответственно TLV файла + TLV подписи.

Совокупность этих TLV и будет файлом выгрузки, которая иногда называется ddd файлом.

Функция для выгрузки файла в формате TLV:

func readFile(cf *CardFile, card *scard.Card) ([]byte, error) {
	var fid, result_len, result []byte
	var resp_len int

	fid = cf.Tag[:]

	resp, err := selectFile(fid, card)
	if err != nil {
		return nil, err
	}

	resp_len = cf.size()
	resp, err = readBinary(resp_len, card)
	if err != nil {
		return nil, err
	}

	result_len = lenToByte(resp_len)
	result = append(fid, 0x00)
	result = append(result, result_len...)
	result = append(result, resp...)

	if cf.Signed {
		_, err = performHash(card)
		if err != nil {
			return nil, err
		}

		signature, err := computeDigitalSignature(card)
		if err != nil {
			return nil, err
		}

		result = append(result, fid...)
		result = append(result, []byte{0x81, 0x00, 0x40}...)
		result = append(result, signature...)
	}

	return result, err
}

Полный пример программы для выгрузки данных с карты находится здесь.

Разбор

Разбор выгруженного файла довольно тривиальный. Нужно отфильтровать все секции с данными, выделить из них знчение файлов и разобрать их в соответствии со спецификацией. Для примера я приведу пример разбора одной секции, остальные будут по аналогии.

Для примера возьмем секцию EF Identification. Если посмотреть в документацию, то секция имеет следующий вид:

Структура секции EF_Identification

Здесь можно увидеть описания стуруктур, которые входят в состав файла, их размер и значения по умолчанию.

Чтобы понять что хранится в этих структурах надо перейти в раздел DATA DICTIONARY в документации и найти соотвтетсвующую струткуру, например CardIdentification хранит информацию по идентификации и в нее входит следующая инфа:

  • cardNumber содержт строку из 16 байт с номером карты
  • cardIssueDate содержит дату выдачи карты размером 4 байта (формат unixtime)
  • cardValidityBegin содержит дату начала действия карты размером 4 байта (формат unixtime)
  • cardExpiryDate содержит дату окончания действия карты размером 4 байта (формат unixtime)

Примерный парсер, реализованный в виде сервиса, можно взять здесь

Заключение

Как видно чтение и разбор данных с карты тахографа довольно простая задача, если понять как правильно работать со спецификацией.

Ссылки

  1. Спецификация на карту тахографа
  2. Сервис для разбора ddd
  3. Ридер для чтения карт тахографа
  4. scard
  5. pcsc-lite
 
comments powered by Disqus