Rekayasa terbalik kode QR untuk bukti vaksinasi

gambar


Ketika Quebec mengumumkan bahwa mereka akan mengirimkan email konfirmasi vaksinasi kepada semua orang yang divaksinasi menggunakan kode QR terlampir, lutut saya sedikit tertekuk. Saya sangat ingin membongkarnya dan menggelengkan kepala pada jumlah informasi kesehatan pribadi yang tidak diragukan lagi akan terungkap dalam prosesnya.



Konfirmasi vaksinasi saya akhirnya tiba, dan hasilnya ... tidak buruk sama sekali. Namun, selalu ada kesenangan dalam peretasan tanpa pengetahuan, jadi saya memutuskan untuk menulis blog tentang pengalaman saya.



Kesan pertama saya adalah, "Ya Tuhan, ini adalah kode QR besar yang tidak perlu." Tidak banyak informasi yang terdaftar di bawah kode QR, jadi mereka mungkin mengenkripsi semua jenis informasi pribadi tanpa sepengetahuan saya. Anda tahu, seperti kode batang di belakang SIM Anda .



Tentu, hal pertama yang saya lakukan adalah memindai kode menggunakan aplikasi QRcode.



hasil
shc:/567629000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000413774



Menarik. Saya pikir akan ada JSON lama yang bagus dalam format biner, tetapi ternyata berbeda. Tampaknya penyandian banyak digit di base64 tidak efisien, tetapi mereka berhasil menjejalkan semuanya menjadi satu kode QR.



Sayangnya, di sinilah bagian nol pengetahuan dari proses berakhir, karena saya memiliki indikator yang cukup jelas tentang ke mana harus pergi selanjutnya: skema URI. Jelas bahwa ini untuk komunikasi dengan beberapa aplikasi pada perangkat orang yang memverifikasi kode yang akan mendaftar untuk memproses skema ini shc :



. Tapi apa skema ini?



Sedikit pencarian membawa saya ke Buku Besar IANA O 'Skema URI di mana shc



terdaftar sebagai pra-pendaftaran dengan nama SMART Health Cards Framework. Jadi ini bukan hanya sesuatu yang dibuat oleh pemerintah Quebec saat bepergian, ini sebenarnya bagian dari proyek nyata! Ini menggembirakan dan tidak terduga.



Ternyata format ini memiliki dokumentasi yang luas dan tujuan desain yang sangat masuk akal , yang menurut saya melegakan bagi pemegang kode tersebut dan sedikit membuat frustrasi ketika seseorang akan menguraikannya secara keseluruhan. Tapi itu tidak masalah! Saya memiliki beberapa kode dan dokumen untuk diikuti, jadi mari kita buka tutupnya dan lihat ke dalamnya.



Menurut dokumen tersebut, menggunakan mode numerik untuk menyandikan data kode QR memberikan kepadatan data yang sedikit lebih tinggi daripada menggunakan mode biner, yang menjelaskan URI angka raksasa daripada string yang disandikan base64 yang lebih masuk akal. Teka-teki pertama terpecahkan.



String panjang angka tampaknya dikodekan dari string ASCII, di mana setiap pasangan digit adalah angka desimal yang merupakan kode karakter. Untuk membuat segalanya lebih membingungkan, output dihitung menggunakan Ord © -45 . Saatnya menulis skrip untuk membalikkan proses ini.



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
      
      





Beberapa hal dapat dipelajari dari ini. Pertama, jelas bahwa PHP masih menjadi bahasa pemrograman cepat saya. Sayangnya, kami akan mengesampingkan wahyu pribadi ini untuk introspeksi lebih lanjut.



Dari sudut pandang teknis, semuanya sekarang tampak seperti string yang disandikan base64. Dan tentu saja doc memberi tahu saya bahwa saya harus melihat JWS, yaitu, token web yang ditandatangani JSON.



Saya akan berhenti sejenak dan mengatakan bahwa ini sebenarnya adalah kasus penggunaan yang bagus untuk JWT. Pada dasarnya, alih-alih token yang tidak berarti atau blok data sensitif raksasa, konsep JWT menyiratkan bahwa saya harus mengharapkan daftar izin yang menjadi hak saya, dibungkus dengan gumpalan yang ditandatangani secara kriptografis oleh penerbit (dalam hal ini, Quebec Santé et Layanan sosial).



Hal yang baik tentang model ini adalah dapat diverifikasi oleh siapa saja dengan kunci publik yang sesuai, bahkan tanpa koneksi internet. Selain itu, jawaban atas pertanyaan "apakah orang ini berhak naik pesawat/menghadiri konser/mengunjungi tempat tinggal lansia?" harus menjawab langsung sebaris, tidak tersirat secara implisit melalui API berpemilik atau sekelompok bidang rahasia yang terkait dengan nomor lot vaksin, dll.



Sekarang saya tidak memiliki salinan kunci publik yang sesuai, tetapi badan harus ditandatangani, tidak dienkripsi, jadi saya masih bisa membacanya.



Mungkin, dalam semangat reverse engineering, saya harus membongkar JWS secara manual, tetapi ini adalah spesifikasi yang terdokumentasi dengan baik (dan yang penting, diterapkan dengan baik). Saya akan malas keluar dan menggunakan paket Komposer web-token / jwt-framework untuk itu .



$ 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..."
}
      
      





Jadi, kami berhasil memecahkan kode tajuk, tetapi tidak ada badan yang masuk. Petunjuk di sini adalah "zip": "DEF" di header, seperti yang juga dinyatakan dalam spec.



payload dikompresi menggunakan algoritma DEFLATE (lihat RFC1951) sebelum menandatangani (perhatikan, ini harus kompresi DEFLATE mentah, tanpa header zlib atau gz




Mari mencoba:



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





NB: kami mendekode dan kemudian mengkode ulang objek JSON untuk menambahkan spasi agar mudah dibaca dengan menentukan konstanta 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"
                                }
                            ]
                        }
                    }
                ]
            }
        }
    }
}
      
      





Ada sedikit lebih banyak informasi pribadi di sana daripada yang benar-benar diperlukan, meskipun saya percaya bahwa menggabungkan nama dan tanggal lahir dengan ID foto adalah proses yang masuk akal. Mereka juga memberikan informasi spesifik tentang vaksin daripada persetujuan spesifik seperti yang saya harapkan. Sekali lagi, ini membuatnya lebih dapat digunakan di seluruh yurisdiksi dan menghilangkan kebutuhan untuk merilis ulang JWS setiap kali kebijakan berubah, yang dalam kasus Quebec terjadi sekitar dua kali seminggu.



Sepanjang analisis ini, saya bertanya-tanya apa yang mungkin mencegah seseorang untuk hanya menunjukkan bukti yang sahih dari vaksinasi orang lain. Karena seluruh tubuh ditandatangani secara kriptografis, Anda tidak dapat mengubah bukti vaksinasi orang lain untuk menambahkan nama Anda, yang berarti menggabungkan bukti vaksinasi dengan ID foto adalah rencana yang sangat masuk akal. Ini pasti akan terjadi di bandara, tetapi saya sangat meragukan bahwa di tempat olahraga, dll. E. Akan meminta ID kedua. Mereka hanya akan memindai kode QR, melihat tanda centang di perangkat mereka, dan melanjutkan ke yang berikutnya.



Satu pemikiran perpisahan: Sementara proses saya diarahkan untuk mencari tahu mana dari data pribadi saya yang dikodekan dalam kode QR, model JWT terkenal mudah dikacaukan, baik dengan lupa memvalidasi sebelum menguraikan data, atau dengan mengizinkan unsigned token ... Jika implementasi tidak menghormati daftar putih pusat penandatangan resmi, akan sangat mudah untuk membuat token yang benar-benar valid yang Anda tandatangani dengan kunci Anda sendiri. Seperti biasa, keamanan model sangat bergantung pada seberapa ketat pihak yang mengandalkan menegakkan standar.



Namun, ternyata satu-satunya informasi pribadi adalah persis informasi yang terkandung dalam dokumen PDF lengkap tentang vaksinasi: nama, tanggal lahir, jenis kelamin (untuk beberapa alasan), serta informasi tentang tanggal dan dosis spesifik yang pemilik diterima pada hari ini. Setelah Anda merasa nyaman dengan implikasi privasi dari menunjukkan SIM Anda di bar, Anda tidak perlu lagi khawatir diminta untuk menunjukkan bukti vaksinasi.



Kodenya adalah sekumpulan sampah, tetapi jika Anda ingin melihat apa yang ada di kode QR Anda sendiri, Anda dapat melihat repositori GitHub untuk posting ini.



All Articles