Pull to refresh
1932.58
Timeweb Cloud
То самое облако

Реверс-инжиниринг QR-кода для доказательства вакцинации

Reading time8 min
Views31K
Original author: Mikkel Paulson
image

Когда Квебек объявил, что будет рассылать электронные письма с подтверждением вакцинации всем, кто был вакцинирован с помощью прикрепленного QR-кода, у меня немного подкосились колени. Мне не терпелось разобрать его на части и покачать головой из-за количества частной медицинской информации, которая, несомненно, будет раскрыта в процессе.

Наконец-то пришло мое подтверждение вакцинации, и результат… вообще-то неплохой. Тем не менее, в хаках с нулевым разглашением всегда есть какое-то удовольствие, поэтому я все равно решил написать о своем опыте в блоге.

Мое первое впечатление было: «Боже мой, это излишне большой QR-код». Под QR-кодом перечислено не так много информации, поэтому они наверняка кодируют все виды личной информации без моего ведома. Знаете, как тот штрих-код на обратной стороне ваших водительских прав.

Естественно, первое, что я сделал, — отсканировал код с помощью приложения QRcode.

результат
shc:/567629000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000413774

Интересно. Я думал, что там будет старый добрый JSON в бинарном формате, но все было иначе. Кажется, что кодировать кучу цифр в base64 неэффективно, но им удалось запихнуть все в один QR-код.

К сожалению, на этом часть процесса с нулевым разглашением заканчивается, потому что у меня есть довольно четкий индикатор, куда двигаться дальше: схема URI. Ясно, что это предназначено для связи с каким-либо приложением на устройстве человека, проверяющего код, который зарегистрируется для обработки этой схемы shc :. Но что это за схема?

Небольшой поиск привел меня к схемам IANA's Big Book O' URI Schemes, где shc указан как предварительно зарегистрированный под названием SMART Health Cards Framework. Так что это не просто то, что правительство Квебека придумало на ходу, это на самом деле часть реального проекта! Это обнадеживающе и неожиданно.

Оказывается, у этого формата есть обширная документация и очень разумные цели дизайна, которые я нахожу как облегчением как держателя такого кода, так и немного разочаровывающим, когда кто-то собирается разобрать его целиком. Но не важно! У меня есть код и документ, которому нужно следовать, так что давайте снимем крышку и заглянем внутрь.

Согласно документу, использование числового режима для кодирования данных QR-кода обеспечивает немного более высокую плотность данных, чем использование двоичного режима, что объясняет гигантский URI чисел, а не более разумную строку в кодировке base64. Первая загадка раскрыта.

Длинная строка чисел, по-видимому, закодирована из строки ASCII, где каждая пара цифр является числом в десятичной системе счисления, которое является кодом символа. Чтобы еще больше запутать, выходные данные вычисляются с использованием Ord © -45. Пришло время написать скрипт, чтобы отреверсить этот процесс.

php -r '$o = ""; foreach (str_split(preg_replace("/[^0-9]/", "", file_get_contents("php://stdin")), 2) as $c) $o .= chr($c + 45); echo $o;' <input.txt | xxd

00000000: 6579 4a72 6157 5169 4f69 4a73 4d33 6c79  eyJraWQiOiJsM3ly
00000010: 5254 4632 526a 646d 6157 5270 6257 5649  RTF2RjdmaWRpbWVI
...
000003b0: 3561 6876 5265 336d 6368 7335 7836 4e49  5ahvRe3mchs5x6NI
000003c0: 4669 3556 5277                           Fi5VRw

Из этого можно извлечь несколько вещей. Во-первых, очевидно, что PHP по-прежнему остается моим быстрым языком программирования. Как печально, но мы отложим это личное откровение для дальнейшего самоанализа.

С технической точки зрения, теперь все выглядит как строки в кодировке base64. И, конечно же, документ говорит мне, что я должен смотреть на JWS, то есть на подписанный веб-токен JSON.

Я сделаю паузу и скажу, что на самом деле это отличный вариант использования JWT. По сути, вместо какого-то бессмысленного токена или гигантского блока конфиденциальных данных концепция JWT подразумевает, что я должен ожидать список разрешений, на которые я имею право, завернутый в большой двоичный объект, который криптографически подписан эмитентом (в данном случае, Quebec Santé et Services sociaux).

Эта модель хороша тем, что ее может проверить любой, у кого есть соответствующий открытый ключ, даже без подключения к Интернету. Кроме того, ответ на вопрос «имеет ли это лицо право сесть на борт самолета / посетить концерт / посетить резиденцию для пожилых людей?» должны напрямую отвечать встроенным, а не косвенно подразумеваемым через проприетарный API или кучу тайных полей, связанных с номерами партий вакцины и т. д.

Теперь у меня нет копии соответствующего открытого ключа, но тело должно быть подписано, а не зашифровано, поэтому я все еще могу его прочитать.

Возможно, в духе реверс-инжиниринга мне следует вручную демонтировать JWS, но это довольно хорошо документированная (и, что немаловажно, хорошо реализованная) спецификация. Я собираюсь пойти на ленивый выход и использовать для этого пакет Composer web-token/jwt-framework.

$ composer require web-token/jwt-framework


<?php
require_once(__DIR__.'/vendor/autoload.php');

use Jose\Component\Signature\Serializer\JWSSerializerManager;
use Jose\Component\Signature\Serializer\CompactSerializer;

$serializerManager = new JWSSerializerManager([
    new CompactSerializer(),
]);

$input_raw = file_get_contents('php://stdin');
$input_token = implode(
    array_map(
        function ($ord) { return chr($ord + 45); },
        str_split(preg_replace('/[^0-9]+/', '', $input_raw), 2)
    )
);

$jws = $serializerManager->unserialize($input_token);
var_dump($jws);

$ cat input.txt | php parse.php
object(Jose\Component\Signature\JWS)#5 (4) {
  ["isPayloadDetached":"Jose\Component\Signature\JWS":private]=>
  bool(false)
  ["encodedPayload":"Jose\Component\Signature\JWS":private]=>
  string(772) "hVNhb9..."
  ["signatures":"Jose\Component\Signature\JWS":private]=>
  array(1) {
    [0]=>
    object(Jose\Component\Signature\Signature)#6 (4) {
      ["encodedProtectedHeader":"Jose\Component\Signature\Signature":private]=>
      string(106) "eyJraW..."
      ["protectedHeader":"Jose\Component\Signature\Signature":private]=>
      array(3) {
        ["kid"]=>
        string(43) "l3yrE1..."
        ["zip"]=>
        string(3) "DEF"
        ["alg"]=>
        string(5) "ES256"
      }
      ["header":"Jose\Component\Signature\Signature":private]=>
      array(0) {
      }
      ["signature":"Jose\Component\Signature\Signature":private]=>
      string(64) "�Q�..."
    }
  }
  ["payload":"Jose\Component\Signature\JWS":private]=>
  string(579) "�Sao..."
}

Итак, мы успешно декодируем заголовок, но тело не приходит. Подсказка здесь — это «zip»: «DEF» в заголовке, как также указано в спецификации.

полезная нагрузка сжимается с помощью алгоритма DEFLATE (см. RFC1951) перед подписанием (обратите внимание, это должно быть «сырое» сжатие DEFLATE, без каких-либо заголовков zlib или gz


Давайте попробуем:

echo json_encode(json_decode(gzinflate($jws->getPayload())), JSON_PRETTY_PRINT);

NB: мы декодируем, а затем перекодируем объект JSON, чтобы добавить пробел для удобства чтения, указав константу JSON_PRETTY_PRINT

{
    "iss": "https:\/\/covid19.quebec.ca\/PreuveVaccinaleApi\/issuer",
    "iat": 1621476457,
    "vc": {
        "@context": [
            "https:\/\/www.w3.org\/2018\/credentials\/v1"
        ],
        "type": [
            "VerifiableCredential",
            "https:\/\/smarthealth.cards#health-card",
            "https:\/\/smarthealth.cards#immunization",
            "https:\/\/smarthealth.cards#covid19"
        ],
        "credentialSubject": {
            "fhirVersion": "1.0.2",
            "fhirBundle": {
                "resourceType": "Bundle",
                "type": "Collection",
                "entry": [
                    {
                        "resource": {
                            "resourceType": "Patient",
                            "name": [
                                {
                                    "family": [
                                        "Paulson"
                                    ],
                                    "given": [
                                        "Mikkel"
                                    ]
                                }
                            ],
                            "birthDate": "1987-xx-xx",
                            "gender": "Male"
                        }
                    },
                    {
                        "resource": {
                            "resourceType": "Immunization",
                            "vaccineCode": {
                                "coding": [
                                    {
                                        "system": "http:\/\/hl7.org\/fhir\/sid\/cvx",
                                        "code": "208"
                                    }
                                ]
                            },
                            "patient": {
                                "reference": "resource:0"
                            },
                            "lotNumber": "xxxxxx",
                            "status": "Completed",
                            "occurrenceDateTime": "2021-xx-xxT04:00:00+00:00",
                            "location": {
                                "reference": "resource:0",
                                "display": "xxxxxxxxxxxxxxxxxx"
                            },
                            "protocolApplied": {
                                "doseNumber": 1,
                                "targetDisease": {
                                    "coding": [
                                        {
                                            "system": "http:\/\/browser.ihtsdotools.org\/?perspective=full&conceptId1=840536004",
                                            "code": "840536004"
                                        }
                                    ]
                                }
                            },
                            "note": [
                                {
                                    "text": "PB COVID-19"
                                }
                            ]
                        }
                    }
                ]
            }
        }
    }
}

Там немного больше личной информации, чем строго необходимо, хотя я полагаю, что сочетание имени и даты рождения с удостоверением личности с фотографией — разумный процесс. Они также предоставляют конкретные сведения о вакцинах, а не конкретные разрешения, как я надеялся. Опять же, это делает все более удобным для использования в разных юрисдикциях и избавляет от необходимости повторно выпускать JWS каждый раз при изменении политики, что в случае Квебека происходит примерно два раза в неделю.

На протяжении всего этого анализа я задавался вопросом, что может помешать кому-то просто предъявить совершенно действительное доказательство вакцинации другого человека. Поскольку все тело подписано криптографической подписью, вы не можете изменить чужое доказательство вакцинации, чтобы добавить свое имя, а это означает, что соединение доказательства вакцинации с удостоверением личности с фотографией — вполне разумный план. Это, безусловно, будет иметь место в аэропортах, но я очень сомневаюсь, что на спортивных объектах и ​​т. Д. Будут просить второе удостоверение личности. Они просто отсканируют QR-код, увидят галочку на своем устройстве и перейдут к следующему.

Одна напутственная мысль: в то время как мой процесс был направлен на выяснение того, какие из моих личных данных кодируются в QR-коде, модель JWT печально известна тем, что ее легко испортить, либо забывая проверить перед анализом данных, либо разрешая токены без подписи. Если реализации не соблюдают центральный белый список авторизованных подписывающих лиц, было бы тривиально легко создать совершенно действительный токен, который вы подписываете своим собственным ключом. Как всегда, безопасность модели действительно зависит от того, насколько строго проверяющая сторона обеспечивает соблюдение стандарта.

Однако оказывается, что единственная личная информация — это именно та информация, которая содержится в полном PDF-документе о вакцинации: имя, дата рождения, пол (по какой-то причине), а также информация о дате и конкретных дозах, которые владелец получил на сегодняшний день. Если вас устраивают последствия для конфиденциальности предъявления водительских прав в баре, вы не должны больше беспокоиться о том, что вас попросят предъявить доказательство вакцинации.

Код представляет собой целую кучу мусора, но если вы хотите увидеть, что находится в вашем собственном QR-коде, вы можете проверить репозиторий GitHub для этого поста.
Tags:
Hubs:
Total votes 21: ↑15 and ↓6+12
Comments11

Articles

Information

Website
timeweb.cloud
Registered
Founded
Employees
201–500 employees
Location
Россия
Representative
Timeweb Cloud