Когда Квебек объявил, что будет рассылать электронные письма с подтверждением вакцинации всем, кто был вакцинирован с помощью прикрепленного QR-кода, у меня немного подкосились колени. Мне не терпелось разобрать его на части и покачать головой из-за количества частной медицинской информации, которая, несомненно, будет раскрыта в процессе.
Наконец-то пришло мое подтверждение вакцинации, и результат… вообще-то неплохой. Тем не менее, в хаках с нулевым разглашением всегда есть какое-то удовольствие, поэтому я все равно решил написать о своем опыте в блоге.
Мое первое впечатление было: «Боже мой, это излишне большой QR-код». Под QR-кодом перечислено не так много информации, поэтому они наверняка извлекают быстрый и кодируют все виды личной информации без моего ведома. Знаете, как тот штрих-код на обратной стороне ваших водительских прав.
Естественно, первое, что я сделал, — отсканировал код с помощью приложения QRcode.
Интересно. Я ожидал, что JSON получится очень много, но здесь происходит кое-что еще. Гигантская стена чисел кажется неэффективной по сравнению с кодировкой base64, но они смогли втиснуть все в один QR-код.
К сожалению, на этом часть процесса с нулевым разглашением заканчивается, потому что у меня есть довольно четкий индикатор, куда двигаться дальше: схема URI. Ясно, что это предназначено для связи с каким-либо приложением на устройстве человека, проверяющего код, который зарегистрируется для обработки этой схемы shc :
. Но что это за схема?
Небольшой поиск привел меня к схемам IANA’s Big Book O’ URI Schemes, где shc
указан как предварительно зарегистрированный под названием SMART Health Cards Framework. Так что это не просто то, что правительство Квебека придумало на ходу, это на самом деле часть реального проекта! Это обнадеживающе и неожиданно.
Оказывается, у этого формата есть обширная документация и очень разумные цели дизайна, которые я нахожу как облегчением как держателя такого кода, так и немного разочаровывающим, когда кто-то собирается разобрать его целиком. Но не важно! У меня есть код и документ, которому нужно следовать, так что давайте снимем крышку и заглянем внутрь.
Согласно документу, использование числового режима для кодирования данных QR-кода обеспечивает немного более высокую плотность данных, чем использование двоичного режима, что объясняет гигантский URI чисел, а не более разумную строку в кодировке base64. Первая загадка раскрыта.
Длинная строка чисел, по-видимому, закодирована из строки ASCII, где каждая пара цифр представляет собой код одного символа в базе 10. Чтобы еще больше запутать, выходные данные вычисляются с использованием 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;'
Из этого можно извлечь несколько вещей. Во-первых, очевидно, что PHP по-прежнему остается моим быстрым языком программирования. Как печально, но мы отложим это личное откровение для дальнейшего самоанализа.
С технической точки зрения, теперь все выглядит как строки в кодировке base64. И, конечно же, документ говорит мне, что я должен смотреть на JWS, то есть на подписанный веб-токен JSON.
Я сделаю паузу и скажу, что на самом деле это отличный вариант использования JWT. По сути, вместо какого-то бессмысленного токена или гигантского блока конфиденциальных данных концепция JWT подразумевает, что я должен ожидать список разрешений, на которые я имею право, завернутый в большой двоичный объект, который криптографически подписан эмитентом (в данном случае, Quebec Santé et Services sociaux).
Эта модель хороша тем, что ее может проверить любой, у кого есть соответствующий открытый ключ, даже без подключения к Интернету. Кроме того, ответ на вопрос «имеет ли это лицо право сесть на борт самолета / посетить концерт / посетить резиденцию для пожилых людей?» должны напрямую отвечать встроенным, а не косвенно подразумеваемым через проприетарный API или кучу тайных полей, связанных с номерами партий вакцины и т. д.
Теперь у меня нет копии соответствующего открытого ключа, но тело должно быть подписано, а не зашифровано, поэтому я все еще могу его прочитать.
Возможно, в духе реверс-инжиниринга мне следует вручную демонтировать JWS, но это довольно хорошо документированная (и, что немаловажно, хорошо реализованная) спецификация. Я собираюсь пойти на ленивый выход и использовать для этого пакет Composer web-token/jwt-framework.
$ composer require web-token/jwt-framework
unserialize($input_token);
var_dump($jws);
$ cat input.txt | php parse.php
object(JoseComponentSignatureJWS)#5 (4) {
["isPayloadDetached":"JoseComponentSignatureJWS":private]=>
bool(false)
["encodedPayload":"JoseComponentSignatureJWS":private]=>
string(772) "hVNhb9..."
["signatures":"JoseComponentSignatureJWS":private]=>
array(1) {
[0]=>
object(JoseComponentSignatureSignature)#6 (4) {
["encodedProtectedHeader":"JoseComponentSignatureSignature":private]=>
string(106) "eyJraW..."
["protectedHeader":"JoseComponentSignatureSignature":private]=>
array(3) {
["kid"]=>
string(43) "l3yrE1..."
["zip"]=>
string(3) "DEF"
["alg"]=>
string(5) "ES256"
}
["header":"JoseComponentSignatureSignature":private]=>
array(0) {
}
["signature":"JoseComponentSignatureSignature":private]=>
string(64) "�Q�..."
}
}
["payload":"JoseComponentSignatureJWS":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+03: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 для этого поста.